Merge pull request #109 from navi-sa/TP-51166

TP-51166 | Team-Page revamp
This commit is contained in:
Pooja Jaiswal
2024-01-22 18:35:37 +05:30
committed by GitHub
35 changed files with 2242 additions and 14 deletions

View File

@@ -12,7 +12,6 @@
"build": "yarn run clear-build && tsc && vite build && yarn run copy-config",
"lint": "eslint --fix \"**/?*.{ts,tsx,js,jsx}\"",
"eslint-check": "eslint \"**/?*.{ts,tsx,js,jsx}\"",
"prepare": "husky install",
"pretty": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,scss,md}\"",
"prettier-check": "prettier --check \"./**/*.{js,jsx,ts,tsx,css,scss,md}\"",
"eslint-check-quiet": "yarn eslint-check --quiet"
@@ -33,7 +32,7 @@
},
"dependencies": {
"@navi/dark-knight": "^1.0.13",
"@navi/web-ui": "^1.59.2",
"@navi/web-ui": "^1.59.4",
"@reduxjs/toolkit": "^1.9.7",
"@stoddabr/react-tableau-embed-live": "^0.3.26",
"antd": "^5.9.4",

View File

@@ -184,6 +184,9 @@
display: inline;
}
.list-wrapper {
display: flex;
}
.hint-text {
margin-top: 24px;
font-size: 12px;

View File

@@ -0,0 +1,7 @@
.team-wrapper {
margin: 24px;
}
.wrapper-class {
display: flex;
}

View File

@@ -0,0 +1,7 @@
.team-details-wrapper {
margin: 20px 0px 0px 79px;
}
.fallback-component {
height: 100px;
margin-left: 450px;
}

View File

@@ -0,0 +1,26 @@
import { useSelector } from 'react-redux';
import { selectSearchTeamData } from '@src/slices/team1Slice';
import FallbackComponent from '@src/components/Fallback';
import TeamMemberDetails from '../partials/TeamMemberDetails';
import SlackDetails from '../partials/SlackDetails';
import styles from './TeamDetails.module.scss';
const TeamDetails: React.FC = () => {
const { isPending } = useSelector(selectSearchTeamData);
if (isPending) {
return (
<div className={styles['fallback-component']}>
<FallbackComponent />
</div>
);
}
return (
<div className={styles['team-details-wrapper']}>
<SlackDetails />
<TeamMemberDetails />
</div>
);
};
export default TeamDetails;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { BorderedInput } from '@navi/web-ui/lib/primitives';
import { SearchIcon } from '@navi/web-ui/lib/icons';
import { SearchInputComponentProps } from '../types';
import styles from './TeamList.module.scss';
const SearchInput: React.FC<SearchInputComponentProps> = ({
value,
onChange,
isAdmin,
}) => {
const containerClass = isAdmin
? styles['search-bar']
: styles['search-input'];
return (
<BorderedInput
LeftInputAdornment={<SearchIcon />}
onChange={onChange}
containerClassName={containerClass}
value={value}
placeholder="Search by team "
/>
);
};
export default SearchInput;

View File

@@ -0,0 +1,66 @@
.team-div {
margin-top: 36px;
padding-top: 8px;
padding-bottom: 8px;
border-radius: 8px;
border: 1px solid var(--navi-color-gray-border);
background: var(--navi-color-gray-bg-primary);
box-shadow: 0px 6px 10px 0px rgba(0, 0, 0, 0.08),
0px 1px 18px 0px rgba(0, 0, 0, 0.06), 0px 3px 5px 0px rgba(0, 0, 0, 0.1);
position: sticky;
}
.search-wrapper {
display: flex;
}
.create-team-btn {
width: 36px;
height: 36px;
padding: 8px;
margin-top: 8px;
margin-left: 8px;
}
@mixin search {
height: 36px;
margin-left: 12px;
}
.search-bar {
@include search;
width: 234px;
}
.search-input {
@include search;
width: 272px;
}
.select-picker-wrapper {
min-width: 300px;
min-height: calc(100vh - 228px) !important;
border: none !important;
box-shadow: none !important;
:hover {
background-color: var(--navi-select-picker-bg-item-selected);
}
}
.select-picker-wrapper::-webkit-scrollbar {
display: none;
}
.icon-wrapper {
display: flex;
gap: 8px;
}
.custom-options {
display: flex;
justify-content: space-between;
font-size: small;
color: var(--navi-color-gray-c2);
height: 29px;
padding-top: 15px;
}
.search-results {
margin-left: 15px;
margin-top: 12px;
color: var(--navi-color-gray-c3);
}
.add-icon {
margin-left: 3px;
}

View File

@@ -0,0 +1,192 @@
import { useEffect, useState, useRef, useReducer } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import { Button, Typography } from '@navi/web-ui/lib/primitives';
import { AddIcon } from '@navi/web-ui/lib/icons';
import PersonIcon from '@src/assets/PersonIcon';
import { fetchTeamDetails, setModalOpen } from '@src/slices/team1Slice';
import { AppDispatch, RootState } from '@src/store';
import { useAuthData } from '@src/services/hooks/useAuth';
import CreateTeam from '@src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam';
import SearchInput from './SearchInput';
import styles from './TeamList.module.scss';
import SelectedPersonIcon from '@src/assets/SelectedPersonIcon';
type Options = {
label: string;
value: number;
additionalData: {
count: number;
isSelected: boolean;
};
};
const CustomTeamOptions = (option: Options): JSX.Element => {
return (
<div className={styles['custom-options']} id={option.value?.toString()}>
<section>{option.label}</section>
<section className={styles['icon-wrapper']}>
<div>{option.additionalData.count}</div>
<div>
{option.additionalData.isSelected ? (
<SelectedPersonIcon />
) : (
<PersonIcon />
)}
</div>
</section>
</div>
);
};
const TeamList: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const Role = useAuthData();
const teamList = useSelector((state: RootState) => state.team1.teams.data);
const defaultTeam = useSelector(
(state: RootState) => state.team1.teams.data[0],
);
const [team, setTeam] = useState<SelectPickerOptionProps>();
const newTeam = useSelector((state: RootState) => state.team1.teams.newTeam);
const [input, setInput] = useState('');
const scroll = useRef<boolean>(false);
const scrollintoView = (event: string): void => {
if (!event) return;
const element = document.getElementById(event);
if (element) {
setTimeout(() => {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 0);
}
};
const options = Array.isArray(teamList)
? teamList.map(item => ({
label: item.name,
value: item.id,
additionalData: {
count: item.slackUserIds?.length,
isSelected:
item.id === team?.value ||
(team?.value ? false : defaultTeam?.id === item.id),
},
}))
: [];
useEffect(() => {
if (!scroll.current && team?.value) {
scroll.current = true;
if (input.length) {
setInput('');
scrollintoView(team?.value?.toString() ?? '');
}
return;
}
}, [team?.value, options]);
useEffect(() => {
scroll.current = false;
if (!scroll.current && newTeam?.toString()?.length) {
scroll.current = true;
if (Array.isArray(teamList) && teamList.length) {
teamList?.forEach(item => {
if (item.id === newTeam) {
setTimeout(() => {
scrollintoView(newTeam?.toString() ?? '');
}, 100);
}
});
}
return;
}
}, [teamList]);
const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
setInput(event.target.value);
};
const createHandler = (): void => {
dispatch(setModalOpen(true));
};
const handleSelectionChange = (
event: SelectPickerOptionProps | SelectPickerOptionProps[],
): void => {
if (!Array.isArray(event)) {
setTeam(event);
dispatch(fetchTeamDetails(event.value.toString()));
scroll.current = false;
}
};
const returnIsFilteredResultEmpty = (): boolean => {
const filteredResult = options.filter(item =>
item.label.toLowerCase().includes(input.toLowerCase()),
);
return !filteredResult.length;
};
return (
<div>
<div className={styles['team-div']}>
<div className={styles['search-wrapper']}>
<SearchInput
value={input}
onChange={handleInputChange}
isAdmin={Role.includes('Admin')}
/>
{Role.includes('Admin') && (
<Button
startAdornment={
<AddIcon
className={styles['add-icon']}
color="var(--navi-color-gray-bg-primary)"
size="xl"
/>
}
onClick={createHandler}
variant="primary"
className={styles['create-team-btn']}
size="small"
>
{''}
</Button>
)}
</div>
{input && (
<div className={styles['team-name']}>
{' '}
<Typography variant="h5" className={styles['search-results']}>
{' '}
SEARCH RESULTS{' '}
</Typography>
</div>
)}
{returnIsFilteredResultEmpty() ? (
<div className={styles['team-name']}>
{' '}
<Typography variant="h5" className={styles['search-results']}>
{' '}
No results found
</Typography>
</div>
) : null}
<SelectPicker
options={options}
onSelectionChange={handleSelectionChange}
multiSelect={false}
updateSearch={input || ''}
wrapperClasses={styles['select-picker-wrapper']}
customOptionTemplate={CustomTeamOptions as any}
selectedValue={team?.value ?? defaultTeam?.id ?? ''}
/>
</div>
<CreateTeam setTeam={setTeam} />
</div>
);
};
export default TeamList;

View File

@@ -0,0 +1,182 @@
import { createURL } from '@src/services/globalUtils';
import { ActionType, AppState, Participant } from './types';
const URL_PREFIX = createURL('/houston');
export const FETCH_TEAM_DATA = `${URL_PREFIX}/teams`;
export const FETCH_ALL_BOTS_DATA = `${URL_PREFIX}/bots`;
export const CREATE_TEAM = `${URL_PREFIX}/teams/add`;
export const FETCH_SINGLE_TEAM_DATA = (payload: string): string => {
return `${URL_PREFIX}/teams/${payload}`;
};
export const UPDATE_TEAM_DATA = (): string => {
return `${URL_PREFIX}/teams`;
};
export const REMOVE_TEAM_MEMBER = (teamId: string, userId: string): string => {
return `${URL_PREFIX}/teams/${teamId}/members/${userId}`;
};
export const MAKE_MANAGER = (teamId: string, userId: string): string => {
return `${URL_PREFIX}/teams/${teamId}/manager/${userId}`;
};
export const regularExpression = /^[a-zA-Z][a-zA-Z0-9_ -]{1,48}[a-zA-Z0-9]$/;
export const validEmail = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const emailRegularExpression = /^[a-zA-Z]+\.[a-zA-Z]+@navi\.com$/;
export const actionTypes = {
SET_OPEN_DIALOG: 'SET_OPEN_DIALOG',
SET_OPEN_MODAL: 'SET_OPEN_MODAL',
SET_EMAIL_IDS: 'SET_EMAIL_IDS',
SET_SELECTED_MEMBER: 'SET_SELECTED_MEMBER',
SET_HOVERED: 'SET_HOVERED',
SET_SLACK_CHANNEL_ID: 'SET_SLACK_CHANNEL_ID',
SET_ONCALL: 'SET_ONCALL',
SET_PSEC_ONCALL: 'SET_PSEC_ONCALL',
SET_INPUT: 'SET_INPUT',
SET_PSEC_INPUT: 'SET_PSEC_INPUT',
SET_OPEN_ONCALL: 'SET_OPEN_ONCALL',
SET_PSEC_OPEN_ONCALL: 'SET_PSEC_OPEN_ONCALL',
SET_TEAM_NAME: 'SET_TEAM_NAME',
SET_TEAM_NAME_ERROR: 'SET_TEAM_NAME_ERROR',
SET_EMAIL_ERROR: 'SET_EMAIL_ERROR',
SET_EMAIL: 'SET_EMAIL',
SET_CLEAR_MODAL: 'SET_CLEAR_MODAL',
SET_ADD_MEMBER_ERROR: 'SET_ADD_MEMBER_ERROR',
};
export const initialState: AppState = {
openDialog: false,
openModal: false,
emailIds: '',
selectedMember: undefined,
hovered: {
ishovered: false,
id: '',
},
slackChannelId: '',
oncall: {
label: '',
value: '',
},
psecOncall: {
label: '',
value: '',
},
input: '',
psecInput: '',
openOnCall: false,
openPsecOnCall: false,
teamName: '',
teamNameError: '',
emailError: '',
email: '',
clearModal: false,
addMemberError: '',
};
export const reducer = (state: AppState, action: ActionType): AppState => {
switch (action.type) {
case actionTypes.SET_OPEN_DIALOG:
return {
...state,
openDialog: action.payload,
};
case actionTypes.SET_OPEN_MODAL:
return {
...state,
openModal: action.payload,
};
case actionTypes.SET_EMAIL_IDS:
return {
...state,
emailIds: action.payload,
};
case actionTypes.SET_SELECTED_MEMBER:
return {
...state,
selectedMember: action.payload,
};
case actionTypes.SET_HOVERED:
return {
...state,
hovered: {
...state.hovered,
...action.payload,
},
};
case actionTypes.SET_SLACK_CHANNEL_ID:
return {
...state,
slackChannelId: action.payload,
};
case actionTypes.SET_ONCALL:
return {
...state,
oncall: action.payload,
};
case actionTypes.SET_PSEC_ONCALL:
return {
...state,
psecOncall: action.payload,
};
case actionTypes.SET_INPUT:
return {
...state,
input: action.payload,
};
case actionTypes.SET_PSEC_INPUT:
return {
...state,
psecInput: action.payload,
};
case actionTypes.SET_OPEN_ONCALL:
return {
...state,
openOnCall: action.payload,
};
case actionTypes.SET_PSEC_OPEN_ONCALL:
return {
...state,
openPsecOnCall: action.payload,
};
case actionTypes.SET_TEAM_NAME:
return {
...state,
teamName: action.payload,
};
case actionTypes.SET_TEAM_NAME_ERROR:
return {
...state,
teamNameError: action.payload,
};
case actionTypes.SET_EMAIL_ERROR:
return {
...state,
emailError: action.payload,
};
case actionTypes.SET_EMAIL:
return {
...state,
email: action.payload,
};
case actionTypes.SET_CLEAR_MODAL:
return {
...state,
teamName: '',
email: '',
teamNameError: '',
emailError: '',
};
case actionTypes.SET_ADD_MEMBER_ERROR:
return {
...state,
addMemberError: action.payload,
};
default:
return state;
}
};

View File

@@ -0,0 +1,62 @@
import { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import { AppDispatch } from '@src/store';
import ErrorBoundary from '@src/components/ErrorBoundary/ErrorBoundary';
import {
TeamState,
fetchTeamDetails,
fetchTeams,
selectSearchTeamData,
setSelectedTeam,
} from '@src/slices/team1Slice';
import TeamDetails from './TeamDetails';
import { fetchAllBots } from './partials/Bots';
import TeamList from './TeamList';
import styles from './Team.module.scss';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
const TeamRevamp: FC = () => {
const dispatch = useDispatch<AppDispatch>();
const teamData: TeamState = useSelector(selectSearchTeamData);
const { data, selectedTeam } = teamData;
const selectedTeamId = selectedTeam?.id || '';
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
useEffect(() => {
dispatch(fetchTeams());
fetchAllBots();
}, []);
useEffect(() => {
if (data) {
dispatch(setSelectedTeam(data[0]));
}
}, []);
useEffect(() => {
if (selectedTeam && selectedTeamId) {
dispatch(fetchTeamDetails(selectedTeamId.toString()));
}
}, [selectedTeam]);
useEffect(() => {
fireEvent(EVENT_NAME.Houston_Team_Land, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
}, []);
return (
<ErrorBoundary>
<div className={styles['team-wrapper']}>
<Typography variant="h3">Teams</Typography>
<div className={styles['wrapper-class']}>
<TeamList />
<TeamDetails />
</div>
</div>
</ErrorBoundary>
);
};
export default TeamRevamp;

View File

@@ -0,0 +1,24 @@
import { ApiService } from '@src/services/api';
import { FETCH_ALL_BOTS_DATA } from '../constants';
import { PickerOptionProps } from '../types';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
let bots: Array<PickerOptionProps> = [];
export const getBots = (): Array<PickerOptionProps> => bots;
export const fetchAllBots = (): void => {
ApiService.get(FETCH_ALL_BOTS_DATA)
.then(response => {
bots = [];
response?.data?.data.map(data => {
bots.push({
label: '@' + data.name,
value: data.id,
});
});
})
.catch(error => {
toast.error(error?.message);
});
};

View File

@@ -0,0 +1,14 @@
.team-wrapper {
padding: 0px 24px 0px 24px;
}
@mixin team-form-wrapper {
height: medium;
margin-bottom: 20px;
gap: 0 8px;
}
.email-wrapper {
@include team-form-wrapper;
}
.input-wrapper {
@include team-form-wrapper;
}

View File

@@ -0,0 +1,172 @@
import { useReducer } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Typography } from '@navi/web-ui/lib/primitives';
import { AlertOutlineIcon } from '@navi/web-ui/lib/icons';
import { BorderedInput, ModalDialog } from '@navi/web-ui/lib/primitives';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { AppDispatch } from '@src/store';
import { ApiService } from '@src/services/api';
import {
fetchTeamDetails,
fetchTeams,
selectSearchTeamData,
setModalOpen,
setNewTeam,
} from '@src/slices/team1Slice';
import {
CREATE_TEAM,
actionTypes,
initialState,
reducer,
} from '@src/Pages/TeamRevamp/constants';
import {
regularExpression,
emailRegularExpression,
validEmail,
} from '@src/Pages/TeamRevamp/constants';
import { CreateTeamProps } from '@src/Pages/TeamRevamp/types';
import styles from './CreateTeam.module.scss';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
const CreateTeam: React.FC<CreateTeamProps> = ({ setTeam }) => {
const dispatch = useDispatch<AppDispatch>();
const [state, dispatchData] = useReducer(reducer, initialState);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const { modalOpen } = useSelector(selectSearchTeamData);
const validateTeamName = (value: string): void => {
value = value.trim();
dispatchData({
type: actionTypes.SET_TEAM_NAME_ERROR,
payload: !regularExpression.test(value)
? 'Min. 3 characters required. Use spaces, _, or - only'
: '',
});
};
const validateEmail = (value: string): void => {
dispatchData({
type: actionTypes.SET_EMAIL_ERROR,
payload: validEmail.test(value)
? !emailRegularExpression.test(value)
? 'Please enter a Navi email ID'
: ''
: '',
});
};
const handleTeamNameChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
const inputValue = e.target.value;
dispatchData({
type: actionTypes.SET_TEAM_NAME,
payload: inputValue,
});
validateTeamName(inputValue);
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const inputValue = e.target.value;
dispatchData({
type: actionTypes.SET_EMAIL,
payload: inputValue,
});
validateEmail(inputValue);
};
const addTeamHandler = (): void => {
fireEvent(EVENT_NAME.Houston_Create_Team, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
ApiService.post(CREATE_TEAM, {
name: state.teamName,
manager_email: state.email,
})
.then(response => {
const toastMessage = `${response?.data?.data?.message}`;
const createdTeamId = response?.data?.data?.id;
const createdTeamName = response?.data?.data?.name;
toast.success(toastMessage);
clearErrors();
dispatch(fetchTeamDetails(createdTeamId.toString()));
setTeam({ label: createdTeamName, value: createdTeamId });
dispatch(setNewTeam(createdTeamId));
dispatch(fetchTeams());
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: ''
}`;
toast.error(toastMessage);
});
};
const clearErrors = (): void => {
dispatch(setModalOpen(false));
dispatchData({
type: actionTypes.SET_CLEAR_MODAL,
payload: '',
});
};
return (
<div className={styles['team-wrapper']}>
{modalOpen && (
<ModalDialog
open={modalOpen}
footerButtons={[
{
label: 'Cancel',
onClick: clearErrors,
},
{
label: 'Submit',
onClick: addTeamHandler,
disabled:
!!state.teamNameError ||
!!state.emailError ||
!state.teamName ||
!state.email,
},
]}
header="Set up new team "
onClose={clearErrors}
>
<div className={styles['input-wrapper']}>
<Typography variant="p4">
<BorderedInput
inputLabel="Team name"
inputSize="full-width"
type="text"
value={state.teamName}
onChange={handleTeamNameChange}
error={state.teamNameError}
Icon={<AlertOutlineIcon color="var(--navi-color-red-base)" />}
placeholder="E.g. NaviPay_Operations"
/>
</Typography>
</div>
<div className={styles['email-wrapper']}>
<Typography variant="p4" color="#F98600">
<BorderedInput
inputLabel="Manager e-mail ID"
inputSize="full-width"
type="text"
value={state.email}
onChange={handleEmailChange}
placeholder="name@navi.com"
error={state.emailError}
Icon={<AlertOutlineIcon color="var(--navi-color-red-base)" />}
/>
</Typography>
</div>
</ModalDialog>
)}
</div>
);
};
export default CreateTeam;

View File

@@ -0,0 +1,76 @@
.team-input-wrapper {
min-width: 320px;
height: 38px;
}
.add-member {
margin: 72px 0px 24px 0px;
}
.member-div {
margin: 32px 0px 0px 0px;
color: var(--navi-color-gray-c3);
}
.on-call-wrapper {
display: flex;
margin-top: 10px;
}
.team-name {
margin: 24px 24px 24px 0px;
color: var(--navi-color-gray-c1);
}
.slack-details-wrapper {
display: inline-flex;
}
.select-picker-wrapper {
min-height: 80px !important;
position: absolute;
z-index: 1;
}
.on-call-handler {
position: relative;
margin-right: 24px;
}
.search-input {
min-width: 320px !important;
}
.info-icon-wrapper {
display: flex;
align-items: center;
margin-top: 5px;
gap: 2px;
}
.info-icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
.update-details {
margin-left: calc(100% - 120px);
margin-bottom: 31px;
margin-top: 12px;
}
.update-details-disabled {
margin-left: calc(100% - 120px);
opacity: 0;
pointer-events: none;
margin-bottom: 31px;
margin-top: 12px;
}
.team-channel-wrapper {
margin-right: 24px;
margin-top: 5px;
}
@mixin slack-label {
color: var(--navi-color-gray-c2);
}
.channel-label,
.oncall-label,
.psec-oncall-label {
@include slack-label;
}
.hint-text {
color: var(--navi-color-gray-c3);
}
.hint-text-disabled {
opacity: 0;
}

View File

@@ -0,0 +1,285 @@
import { useEffect, useRef, MutableRefObject, useReducer } from 'react';
import {
BorderedInput,
Button,
Tooltip,
Typography,
} from '@navi/web-ui/lib/primitives';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { AlertOutlineIcon } from '@navi/web-ui/lib/icons';
import useOutsideClick from '@src/services/hooks/useOustideClick';
import { useAuthData } from '@src/services/hooks/useAuth';
import useTeamApis from '../../useTeamApis';
import { actionTypes, initialState, reducer } from '../../constants';
import { TeamsDetail } from '../../types';
import { getBots } from '../Bots';
import useGetTeamDetailsConstants from '../../util';
import styles from './Slack.module.scss';
const TeamDetails: React.FC = () => {
const [state, dispatchData] = useReducer(reducer, initialState);
const Role = useAuthData();
const { updateDetails } = useTeamApis();
const {
userEmail,
managerEmail,
teamDetails,
teamData,
DEFAULT_TEAM_ONCALL,
DEFAULT_TEAM_ONCALL_ID,
DEFAULT_TEAM_PSEC_ID,
DEFAULT_PSEC_ONCALL,
webChannelId,
} = useGetTeamDetailsConstants();
const searchRef = useRef<HTMLInputElement>(null);
useEffect(() => {
dispatchData({
type: actionTypes.SET_INPUT,
payload: '',
});
dispatchData({
type: actionTypes.SET_PSEC_INPUT,
payload: '',
});
}, [teamData]);
const handleslackChannelId = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
dispatchData({
type: actionTypes.SET_SLACK_CHANNEL_ID,
payload: e.target.value,
});
};
const handleOncallChange = (event): void => {
dispatchData({
type: actionTypes.SET_ONCALL,
payload: {
label: event.label,
value: event.value,
},
});
dispatchData({
type: actionTypes.SET_INPUT,
payload: event.label,
});
dispatchData({
type: actionTypes.SET_OPEN_ONCALL,
payload: false,
});
};
const handlePsecOncall = (event): void => {
dispatchData({
type: actionTypes.SET_PSEC_ONCALL,
payload: {
label: event.label,
value: event.value,
},
});
dispatchData({
type: actionTypes.SET_PSEC_INPUT,
payload: event.label,
});
dispatchData({
type: actionTypes.SET_PSEC_OPEN_ONCALL,
payload: false,
});
};
const handleOncallOutsideClick = (): void => {
dispatchData({
type: actionTypes.SET_OPEN_ONCALL,
payload: false,
});
};
const handlePsecOncallOutsideClick = (): void => {
dispatchData({
type: actionTypes.SET_PSEC_OPEN_ONCALL,
payload: false,
});
};
const refOncall = useOutsideClick({
callback: handleOncallOutsideClick,
}) as MutableRefObject<HTMLDivElement>;
const refPsecOncall = useOutsideClick({
callback: handlePsecOncallOutsideClick,
}) as MutableRefObject<HTMLDivElement>;
const handleOncallPicker = (): void => {
if (!isUserParticipant(teamDetails) && !Role.includes('Admin')) return;
dispatchData({
type: actionTypes.SET_OPEN_ONCALL,
payload: !state.openOnCall,
});
};
const handleOpenPsecOnCallPicker = (): void => {
if (!isUserParticipant(teamDetails) && !Role.includes('Admin')) return;
dispatchData({
type: actionTypes.SET_PSEC_OPEN_ONCALL,
payload: !state.openPsecOnCall,
});
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
dispatchData({
type: actionTypes.SET_INPUT,
payload: e.target.value,
});
};
const handlePsecInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
dispatchData({
type: actionTypes.SET_PSEC_INPUT,
payload: e.target.value,
});
};
const isUserParticipant = (teamDetails: TeamsDetail): boolean | undefined => {
const participantEmails = teamDetails?.participants?.map(
(participant: any) => participant.email,
);
return participantEmails?.includes(userEmail);
};
const isHintEnabled =
isUserParticipant(teamDetails) ||
Role.includes('Admin') ||
userEmail === managerEmail;
const hintClassName = isHintEnabled
? styles['hint-text']
: styles['hint-text-disabled'];
return (
<div className={styles['team-details-wrapper']}>
<Typography variant="h1" className={styles['team-name']}>
{teamDetails?.name}
</Typography>{' '}
<Typography variant="h5" className={styles['member-div']}>
SLACK DETAILS
</Typography>{' '}
<div className={styles['slack-details-wrapper']}>
<div className={styles['team-channel-wrapper']}>
<div className={styles['info-icon-wrapper']}>
<Typography variant="h6" className={styles['channel-label']}>
Team channel
</Typography>
<div className={styles['info-icon']}>
<Tooltip
text="Slack channel I.D. can be found at the bottom of
the channel details window"
withPointer
position={'right'}
>
<AlertOutlineIcon />
</Tooltip>
</div>
</div>
<BorderedInput
inputLabel=""
inputSize="medium"
placeholder="Enter channel I.D"
onChange={handleslackChannelId}
containerClassName={styles['team-input-wrapper']}
value={webChannelId}
hintTextClasses={hintClassName}
hintMsg="Channel name populates after entering id"
disabled={
!isUserParticipant(teamDetails) && !Role.includes('Admin')
}
/>
</div>
<div className={styles['on-call-wrapper']}>
<div ref={refOncall}>
<Typography variant="h6" className={styles['oncall-label']}>
Team on call
</Typography>{' '}
<div
onClick={handleOncallPicker}
className={styles['on-call-handler']}
>
<BorderedInput
onChange={handleInputChange}
placeholder="@oncall_handle"
containerClassName={styles['search-input']}
value={state.input || DEFAULT_TEAM_ONCALL}
disabled={
!isUserParticipant(teamDetails) && !Role.includes('Admin')
}
></BorderedInput>
</div>
{state.openOnCall && (
<SelectPicker
options={getBots()}
onSelectionChange={handleOncallChange}
selectedValue={state.oncall.value || DEFAULT_TEAM_ONCALL_ID}
multiSelect={false}
updateSearch={
state.input || searchRef.current ? state.input : ''
}
wrapperClasses={styles['select-picker-wrapper']}
></SelectPicker>
)}
</div>
<div ref={refPsecOncall}>
<Typography variant="h6" className={styles['psec-oncall-label']}>
PSEC on call
</Typography>
<div
onClick={handleOpenPsecOnCallPicker}
className={styles['on-call-handler']}
>
<BorderedInput
onChange={handlePsecInputChange}
placeholder="@psec_oncall_handle"
containerClassName={styles['search-input']}
value={state.psecInput || DEFAULT_PSEC_ONCALL}
disabled={
!isUserParticipant(teamDetails) && !Role.includes('Admin')
}
></BorderedInput>
</div>
{state.openPsecOnCall && (
<SelectPicker
options={getBots()}
onSelectionChange={handlePsecOncall}
multiSelect={false}
selectedValue={state.psecOncall.value || DEFAULT_TEAM_PSEC_ID}
updateSearch={
state.psecInput || searchRef.current ? state.psecInput : ''
}
wrapperClasses={styles['select-picker-wrapper']}
></SelectPicker>
)}
<div
className={
managerEmail === userEmail ||
isUserParticipant(teamDetails) ||
Role.includes('Admin')
? styles['update-details']
: styles['update-details-disabled']
}
>
<Button
variant="text"
onClick={() => {
updateDetails(
state.slackChannelId,
state.oncall.value,
state.psecOncall.value,
);
}}
disabled={
!state.slackChannelId && !state.psecInput && !state.input
}
>
Update details
</Button>
</div>
</div>
</div>
</div>
</div>
);
};
export default TeamDetails;

View File

@@ -0,0 +1,105 @@
.member-details-wrapper {
width: 100%;
}
.add-member {
margin-bottom: 24px;
}
.details-wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
justify-content: space-around;
gap: 12px;
}
.item-details {
margin-top: 12px;
margin-left: 10px;
width: calc(100% - 48px);
}
.details-container {
display: flex;
width: 252px;
height: 68px;
}
.icon-wrapper {
margin-top: 12px;
width: 32px;
margin-left: 6px;
}
.items-wrapper {
width: 252px;
border-radius: 8px;
height: 95px;
}
.team-member-view .make-manger-button,
.delete-icon {
display: none;
}
.items-wrapper .make-manger-button,
.items-wrapper .delete-icon {
display: none;
}
.items-wrapper:hover .make-manger-button {
display: block;
margin-top: 4px;
}
.items-wrapper:hover .delete-icon {
display: block;
margin-top: 12px;
width: 16px;
margin-right: 12px;
}
.items-wrapper:hover {
background-color: var(--navi-color-blue-bg);
cursor: pointer;
}
.manager-wrapper {
width: 253px;
border-radius: 8px;
}
.remove-button {
background-color: var(--navi-color-red-base);
height: 36px;
width: 77px;
color: var(--navi-color-gray-bg-primary);
}
.cancel-button {
background: var(--navi-color-gray-bg-primary);
height: 36px;
width: 77px;
color: var(--navi-color-gray-c2);
margin-right: 12px;
border: 1px solid var(--navi-color-gray-border);
border-radius: 6px;
margin-right: 16px;
}
.team-input-wrapper {
width: 510px;
height: 36px;
margin-top: 12px;
}
.footer-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 32px;
}
.input-wrapper-child {
display: flex;
}
.horizontal-line {
display: flex;
}
.hr-tag {
width: 100%;
margin-top: 12px;
margin-left: 5px;
border: none;
border-top: 2px solid var(--navi-color-gray-border);
}
.members-tag {
color: var(--navi-color-gray-c3);
}

View File

@@ -0,0 +1,295 @@
import { useReducer } from 'react';
import {
BorderedInput,
Button,
Typography,
ModalDialog,
} from '@navi/web-ui/lib/primitives';
import PersonIconFill from '@src/assets/PersonIconFill';
import ManagerIconFill from '@src/assets/ManagerIconFill';
import MemberIcon from '@src/assets/MemberIcon';
import DeleteIcon from '@src/assets/DeleteIcon';
import { useAuthData } from '@src/services/hooks/useAuth';
import { useGetTeamDetailsConstants } from '@src/Pages/TeamRevamp/util';
import useTeamApis from '../../useTeamApis';
import {
actionTypes,
emailRegularExpression,
initialState,
reducer,
validEmail,
} from '@src/Pages/TeamRevamp/constants';
import { TeamsDetail } from '@src/Pages/TeamRevamp/types';
import styles from './TeamMemberDetails.module.scss';
type MemberType = {
id: string;
name: string;
email: string;
};
const TeamMemberDetails: React.FC = () => {
const [state, dispatchData] = useReducer(reducer, initialState);
const Role = useAuthData();
const { teamId, userEmail, managerEmail, manager, teamMembers, teamDetails } =
useGetTeamDetailsConstants();
const { addMemberHandler, removeTeamMember, makeManager } = useTeamApis();
const isUserParticipant = (teamDetails: TeamsDetail): boolean | undefined => {
const participantEmails = teamDetails?.participants?.map(
(participant: any) => participant.email,
);
return participantEmails?.includes(userEmail);
};
const validateEmail = (value: string): void => {
const emailAddresses = value.split(',').map(email => email.trim());
const errorMessage = emailAddresses
.map(email =>
validEmail.test(email)
? emailRegularExpression.test(email)
? ''
: 'Please enter a Navi email ID'
: '',
)
.find(errorMessage => errorMessage !== '');
dispatchData({
type: actionTypes.SET_ADD_MEMBER_ERROR,
payload: errorMessage || '',
});
};
const handleAddMember = (e: React.ChangeEvent<HTMLInputElement>): void => {
const inputValue = e.target.value;
dispatchData({
type: actionTypes.SET_EMAIL_IDS,
payload: inputValue,
});
validateEmail(inputValue);
};
const onKeyPressClickHandler = (event: React.KeyboardEvent): void => {
if (state.addMemberError) {
return;
}
if (event.key === 'Enter') {
addMemberHandler(state.emailIds);
dispatchData({
type: actionTypes.SET_EMAIL_IDS,
payload: '',
});
}
dispatchData({
type: actionTypes.SET_EMAIL_IDS,
payload: '',
});
};
const handleOpenModal = (member: MemberType): void => {
dispatchData({
type: actionTypes.SET_SELECTED_MEMBER,
payload: member,
});
dispatchData({
type: actionTypes.SET_OPEN_MODAL,
payload: true,
});
};
const handleResetModal = (): void => {
dispatchData({
type: actionTypes.SET_OPEN_MODAL,
payload: false,
});
};
const handleOpenDialog = (member: MemberType): void => {
dispatchData({
type: actionTypes.SET_SELECTED_MEMBER,
payload: member,
});
dispatchData({
type: actionTypes.SET_OPEN_DIALOG,
payload: true,
});
};
const handleResetDialog = (): void => {
dispatchData({
type: actionTypes.SET_OPEN_DIALOG,
payload: false,
});
};
const handleHoverchange = (item: MemberType): void => {
if (userEmail === managerEmail || Role.includes('Admin')) {
dispatchData({
type: actionTypes.SET_HOVERED,
payload: {
ishovered: false,
id: item.id,
},
});
}
};
const handleHoverIcon = (item: MemberType): void => {
if (userEmail === managerEmail || Role.includes('Admin')) {
dispatchData({
type: actionTypes.SET_HOVERED,
payload: {
ishovered: true,
id: item.id,
},
});
}
};
const computedClassName = (item: MemberType): string => {
return (
(manager?.name === item.name && styles['manager-wrapper']) ||
(userEmail !== managerEmail &&
!Role.includes('Admin') &&
styles['team-member-view']) ||
styles['items-wrapper']
);
};
return (
<div>
<div className={styles['add-member']}>
<div className={styles['horizontal-line']}>
<Typography variant="h5" className={styles['members-tag']}>
MEMBERS
</Typography>
<hr className={styles['hr-tag']}></hr>{' '}
</div>
{(isUserParticipant(teamDetails) ||
Role.includes('Admin') ||
userEmail === managerEmail) && (
<BorderedInput
placeholder={'Add comma separated e-mail ID(s)'}
onChange={handleAddMember}
hintMsg={state.addMemberError ? '' : 'Press enter to add e-mails'}
onKeyPress={onKeyPressClickHandler}
value={state.emailIds}
containerClassName={styles['team-input-wrapper']}
disabled={
!isUserParticipant(teamDetails) && !Role.includes('Admin')
}
error={state.addMemberError}
/>
)}
</div>
<div key={teamId} className={styles['member-details-wrapper']}>
<div key={teamId} className={styles['details-wrapper']}>
{teamMembers?.map(item => (
<div
className={computedClassName(item)}
key={item.id}
onMouseEnter={() => handleHoverIcon(item)}
onMouseLeave={() => handleHoverchange(item)}
>
<div className={styles['details-container']}>
<div className={styles['icon-wrapper']}>
{manager?.name === item.name ? (
<ManagerIconFill />
) : (
<div>
{state.hovered.ishovered &&
state.hovered.id === item.id ? (
<MemberIcon />
) : (
<PersonIconFill />
)}
</div>
)}
</div>
<div key={item.id} className={styles['item-details']}>
{' '}
<Typography variant="p3"> {item.name}</Typography>
<Typography variant="p5"> {item.email}</Typography>
<Typography variant="p5">
{manager?.name === item.name ? 'Manager' : ''}
</Typography>{' '}
<div className={styles['make-manger-button']}>
{manager?.name !== item.name && (
<Button
variant="text"
onClick={() => {
handleOpenModal(item);
}}
>
Make manager{' '}
</Button>
)}
</div>
</div>
{manager?.name !== item.name && (
<div
id={item.id}
onClick={() => {
handleOpenDialog(item);
}}
className={styles['delete-icon']}
>
<DeleteIcon />
</div>
)}
</div>
</div>
))}
</div>
<ModalDialog
open={state.openModal}
footerButtons={[
{
label: 'Cancel',
onClick: handleResetModal,
},
{
label: 'Update',
onClick: () => {
makeManager(state.selectedMember?.id.toString() || '');
handleResetModal();
},
},
]}
header="Update manager? "
onClose={handleResetModal}
>
<div className={styles['input-wrapper']}>
<Typography variant="p4">
Are you sure you want to make {`${state.selectedMember?.name}`}{' '}
manager of this team?
</Typography>
</div>
</ModalDialog>
<ModalDialog
open={state.openDialog}
header="Remove member? "
onClose={handleResetDialog}
>
<div className={styles['input-wrapper']}>
<Typography variant="p4">
Are you sure you want to remove {`${state.selectedMember?.name}`}{' '}
from this team?
</Typography>
</div>
<div className={styles['footer-wrapper']}>
<Button
variant="text"
onClick={handleResetDialog}
className={styles['cancel-button']}
>
Cancel
</Button>
<Button
variant="text"
onClick={() => {
removeTeamMember(state.selectedMember?.id.toString() || '');
handleResetDialog();
}}
className={styles['remove-button']}
>
Remove
</Button>
</div>
</ModalDialog>
</div>
</div>
);
};
export default TeamMemberDetails;

View File

@@ -0,0 +1,125 @@
import { ChangeEvent } from 'react';
import { TeamState } from '@src/slices/team1Slice';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
export interface SearchInputComponentProps {
value: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
isAdmin: boolean;
}
export interface useTeamApiProps {
addMemberHandler: (emailIds: string) => void;
removeTeamMember: (memberId: string) => void;
makeManager: (memberId: string) => void;
updateDetails: (
slackChannelId: string,
oncall: string,
psecOncall: string,
) => void;
}
export interface TeamsData {
id?: string;
name?: string;
slackUserIds?: Array<string>;
lastUpdatedAt?: string;
expand?: boolean;
}
export interface CreateTeamProps {
setTeam: React.Dispatch<
React.SetStateAction<SelectPickerOptionProps | undefined>
>;
}
export interface BotsData {
id: string;
name: string;
}
export interface TeamFormProps {
teamId: string;
isExpanded: boolean;
setLastUpdatedAt: (value: string) => void;
}
export const slackUserOptions = [
{
label: 'Email-id',
value: 'WorkEmailIds',
},
{
label: 'Slack user-id',
value: 'SlackUserIds',
},
];
export const userInputPlaceholders = {
'Email-id': 'email ids',
'Slack user-id': 'slack user ids',
};
export interface PickerOptionProps {
label: string;
value: string;
}
export interface Details {
teamId: number | undefined;
userEmail: string | undefined;
managerEmail: string | undefined;
manager: Participant | undefined;
teamMembers: any;
teamDetails: TeamsDetail;
teamData: TeamState;
DEFAULT_TEAM_ONCALL: string;
DEFAULT_TEAM_ONCALL_ID: string;
DEFAULT_TEAM_PSEC_ID: string;
DEFAULT_PSEC_ONCALL: string;
webChannelId: string;
}
export interface Participant {
name: string;
email: string;
id: string;
}
export interface TeamsDetail {
id?: number;
lastUpdatedAt?: string;
managerId?: string;
name?: string;
oncall?: { id: string; name: string };
participants?: Array<Participant>;
pse_oncall?: { id: string; name: string };
slackUserIds?: string[];
webhookSlackChannelId?: string;
webhookSlackChannelName?: string;
}
export interface AppState {
openDialog: boolean;
openModal: boolean;
emailIds: string;
selectedMember?: Participant;
hovered: {
ishovered: boolean;
id: string;
};
slackChannelId: string;
oncall: {
label: string;
value: string;
};
psecOncall: {
label: string;
value: string;
};
input: string;
psecInput: string;
openOnCall: boolean;
openPsecOnCall: boolean;
teamName: string;
teamNameError: string;
emailError: string;
email: string;
clearModal: boolean;
addMemberError: string;
}
export interface ActionType {
type: string;
payload: any;
}

View File

@@ -0,0 +1,147 @@
import { useReducer } from 'react';
import { useDispatch } from 'react-redux';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { AppDispatch } from '@src/store';
import { ApiService } from '@src/services/api';
import { fetchTeamDetails, fetchTeams } from '@src/slices/team1Slice';
import {
MAKE_MANAGER,
REMOVE_TEAM_MEMBER,
UPDATE_TEAM_DATA,
actionTypes,
initialState,
reducer,
} from './constants';
import { useGetTeamDetailsConstants } from './util';
import { useTeamApiProps } from './types';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
const useTeamApis = (): useTeamApiProps => {
const dispatch = useDispatch<AppDispatch>();
const { teamId } = useGetTeamDetailsConstants();
const [state, dispatchData] = useReducer(reducer, initialState);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const addMemberHandler = (emailIds: string): void => {
fireEvent(EVENT_NAME.Houston_Add_Member, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
const endPoint = UPDATE_TEAM_DATA();
const finalSlackData = emailIds.includes(',')
? emailIds.split(',').map(item => item?.trim())
: [emailIds];
console.log(teamId, emailIds, finalSlackData);
ApiService.post(endPoint, {
id: teamId,
workEmailIds: finalSlackData,
})
.then(response => {
toast.success(response?.data?.data);
dispatch(fetchTeamDetails(teamId?.toString() || ''));
dispatch(fetchTeams());
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: 'Something went wrong, please try again later'
}`;
toast.error(toastMessage);
});
};
const removeTeamMember = (memberId: string): void => {
fireEvent(EVENT_NAME.Houston_Remove_Member, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
const endpoint = REMOVE_TEAM_MEMBER(teamId?.toString() || '', memberId);
ApiService.delete(endpoint)
.then(response => {
if (response.status === 200) {
toast.success(response.data.data);
dispatch(fetchTeamDetails(teamId?.toString() || ''));
dispatch(fetchTeams());
} else {
toast.error(response.data.error.message);
}
})
.catch(error => {
toast.error(`Error removing member from team : ${error.message}`);
});
};
const makeManager = (memberId: string): void => {
fireEvent(EVENT_NAME.Houston_Make_Manager, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
const endpoint = MAKE_MANAGER(teamId?.toString() || '', memberId);
ApiService.patch(endpoint, {})
.then(response => {
if (response.status === 200) {
toast.success(response.data.data);
dispatch(fetchTeamDetails(teamId?.toString() || ''));
} else {
toast.error(response.data.error.message);
}
})
.catch(error => {
toast.error(`Error in making manager of team: ${error.message}`);
});
};
const updateDetails = (
slackChannelId: string,
oncall: string,
psecOncall: string,
): void => {
if (oncall) {
fireEvent(EVENT_NAME.Houston_Add_Oncall, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
}
if (psecOncall) {
fireEvent(EVENT_NAME.Houston_Add_Pseconcall, {
screen_name: SCREEN_NAME.TEAM_PAGE,
});
}
const endPoint = UPDATE_TEAM_DATA();
ApiService.post(endPoint, {
id: teamId,
webhook_slack_channel: slackChannelId,
on_call_handle: oncall,
pse_on_call_id: psecOncall,
})
.then(response => {
toast.success(response?.data?.data);
dispatch(fetchTeamDetails(teamId?.toString() || ''));
dispatchData({
type: actionTypes.SET_OPEN_ONCALL,
payload: false,
});
dispatchData({
type: actionTypes.SET_PSEC_OPEN_ONCALL,
payload: false,
});
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: 'Something went wrong,Pls try again later'
}`;
toast.error(toastMessage);
});
};
return {
addMemberHandler,
removeTeamMember,
makeManager,
updateDetails,
};
};
export default useTeamApis;

View File

@@ -0,0 +1,57 @@
import { useSelector } from 'react-redux';
import { TeamState, selectSearchTeamData } from '@src/slices/team1Slice';
import { Details } from './types';
export const useTeamDetails = (): TeamState => {
return useSelector(selectSearchTeamData);
};
export const useGetTeamDetailsConstants = (): Details => {
const userData = JSON.parse(localStorage.getItem('user-data') || '{}');
const teamData = useTeamDetails();
const teamDetails = teamData.teamDetails;
const teamId = teamDetails?.id;
const userEmail = userData?.emailId || '';
const managerEmail = teamDetails?.participants?.find(
item => item.id === teamDetails?.managerId,
)?.email;
const manager = teamDetails?.participants?.find(
item => item.id === teamDetails?.managerId,
);
const teamMembers =
teamDetails?.participants?.map(item => ({
name: item.name,
email: item.email,
id: item.id,
})) || [];
const DEFAULT_TEAM_ONCALL = teamDetails?.oncall?.name
? `@${teamDetails?.oncall?.name}`
: '';
const DEFAULT_TEAM_ONCALL_ID = teamDetails?.oncall?.id || '';
const DEFAULT_TEAM_PSEC_ID = teamDetails?.pse_oncall?.id || '';
const DEFAULT_PSEC_ONCALL =
teamDetails?.pse_oncall && teamDetails?.pse_oncall?.name != ''
? '@' + teamDetails?.pse_oncall?.name
: '';
const webChannelId =
teamDetails?.webhookSlackChannelName !== 'not found'
? `@${teamDetails?.webhookSlackChannelName}`
: teamDetails?.webhookSlackChannelId
? teamDetails?.webhookSlackChannelId
: '';
return {
teamId,
userEmail,
managerEmail,
manager,
teamMembers,
teamDetails,
teamData,
DEFAULT_TEAM_ONCALL,
DEFAULT_TEAM_ONCALL_ID,
DEFAULT_TEAM_PSEC_ID,
DEFAULT_PSEC_ONCALL,
webChannelId,
};
};
export default useGetTeamDetailsConstants;

38
src/assets/AlertIcon.tsx Normal file
View File

@@ -0,0 +1,38 @@
import React, { FC, useState } from 'react';
import { IconProps } from './types';
const AlertIcon: FC<IconProps> = ({
width = '24',
height = '24',
onClick,
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<mask
id="mask0_3268_65747"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="24"
height="24"
>
<rect width="24" height="24" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_3268_65747)">
<path
d="M12 17C12.2833 17 12.521 16.904 12.713 16.712C12.9043 16.5207 13 16.2833 13 16C13 15.7167 12.9043 15.479 12.713 15.287C12.521 15.0957 12.2833 15 12 15C11.7167 15 11.4793 15.0957 11.288 15.287C11.096 15.479 11 15.7167 11 16C11 16.2833 11.096 16.5207 11.288 16.712C11.4793 16.904 11.7167 17 12 17ZM11 12C11 12.5523 11.4477 13 12 13C12.5523 13 13 12.5523 13 12V8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8V12ZM12 22C10.6167 22 9.31667 21.7373 8.1 21.212C6.88333 20.6873 5.825 19.975 4.925 19.075C4.025 18.175 3.31267 17.1167 2.788 15.9C2.26267 14.6833 2 13.3833 2 12C2 10.6167 2.26267 9.31667 2.788 8.1C3.31267 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.31233 8.1 2.787C9.31667 2.26233 10.6167 2 12 2C13.3833 2 14.6833 2.26233 15.9 2.787C17.1167 3.31233 18.175 4.025 19.075 4.925C19.975 5.825 20.6873 6.88333 21.212 8.1C21.7373 9.31667 22 10.6167 22 12C22 13.3833 21.7373 14.6833 21.212 15.9C20.6873 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6873 15.9 21.212C14.6833 21.7373 13.3833 22 12 22Z"
fill="#F98600"
/>
</g>
</svg>
);
};
export default AlertIcon;

26
src/assets/DeleteIcon.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { FC } from 'react';
import { IconProps } from './types';
const DeleteIcon: FC<IconProps> = ({
width = '24',
height = '24',
onClick,
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="16"
viewBox="0 0 14 16"
fill="none"
>
<path
d="M2.83333 15.5C2.37499 15.5 1.98277 15.3369 1.65666 15.0108C1.32999 14.6842 1.16666 14.2917 1.16666 13.8333V3C0.93055 3 0.732495 2.92028 0.572495 2.76083C0.413051 2.60083 0.333328 2.40278 0.333328 2.16667C0.333328 1.93056 0.413051 1.7325 0.572495 1.5725C0.732495 1.41306 0.93055 1.33333 1.16666 1.33333H4.49999C4.49999 1.09722 4.57999 0.899167 4.74 0.739167C4.89944 0.579722 5.09722 0.5 5.33333 0.5H8.66666C8.90277 0.5 9.10083 0.579722 9.26083 0.739167C9.42027 0.899167 9.49999 1.09722 9.49999 1.33333H12.8333C13.0694 1.33333 13.2672 1.41306 13.4267 1.5725C13.5867 1.7325 13.6667 1.93056 13.6667 2.16667C13.6667 2.40278 13.5867 2.60083 13.4267 2.76083C13.2672 2.92028 13.0694 3 12.8333 3V13.8333C12.8333 14.2917 12.6703 14.6842 12.3442 15.0108C12.0175 15.3369 11.625 15.5 11.1667 15.5H2.83333ZM2.83333 3V13.8333H11.1667V3H2.83333ZM4.49999 11.3333C4.49999 11.5694 4.57999 11.7672 4.74 11.9267C4.89944 12.0867 5.09722 12.1667 5.33333 12.1667C5.56944 12.1667 5.7675 12.0867 5.9275 11.9267C6.08694 11.7672 6.16666 11.5694 6.16666 11.3333V5.5C6.16666 5.26389 6.08694 5.06583 5.9275 4.90583C5.7675 4.74639 5.56944 4.66667 5.33333 4.66667C5.09722 4.66667 4.89944 4.74639 4.74 4.90583C4.57999 5.06583 4.49999 5.26389 4.49999 5.5V11.3333ZM7.83333 11.3333C7.83333 11.5694 7.91333 11.7672 8.07333 11.9267C8.23277 12.0867 8.43055 12.1667 8.66666 12.1667C8.90277 12.1667 9.10083 12.0867 9.26083 11.9267C9.42027 11.7672 9.49999 11.5694 9.49999 11.3333V5.5C9.49999 5.26389 9.42027 5.06583 9.26083 4.90583C9.10083 4.74639 8.90277 4.66667 8.66666 4.66667C8.43055 4.66667 8.23277 4.74639 8.07333 4.90583C7.91333 5.06583 7.83333 5.26389 7.83333 5.5V11.3333Z"
fill="#E92C2C"
/>
</svg>
);
};
export default DeleteIcon;

View File

@@ -1,4 +1,4 @@
import React, { FC } from 'react';
import { FC } from 'react';
import { IconProps } from './types';

View File

@@ -0,0 +1,28 @@
import React, { FC } from 'react';
import { IconProps } from './types';
const ManagerIconFill: FC<IconProps> = ({
width = '32',
height = '32',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
>
<rect width="32" height="32" rx="16" fill="#E6F1FF" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 7C18.2133 7 20 8.78667 20 11C20 13.2133 18.2133 15 16 15C13.7867 15 12 13.2133 12 11C12 8.78667 13.7867 7 16 7ZM16 25.9333C12.6667 25.9333 9.72 24.2267 8 21.64C8.04 18.9867 13.3333 17.5333 16 17.5333C18.6533 17.5333 23.96 18.9867 24 21.64C22.28 24.2267 19.3333 25.9333 16 25.9333Z"
fill="#0276FE"
/>
</svg>
);
};
export default ManagerIconFill;

28
src/assets/MemberIcon.tsx Normal file
View File

@@ -0,0 +1,28 @@
import React, { FC } from 'react';
import { IconProps } from './types';
const MemberIcon: FC<IconProps> = ({
width = '32',
height = '32',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
>
<rect width="32" height="32" rx="16" fill="#CCE4FF" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 7C18.2133 7 20 8.78667 20 11C20 13.2133 18.2133 15 16 15C13.7867 15 12 13.2133 12 11C12 8.78667 13.7867 7 16 7ZM16 25.9333C12.6667 25.9333 9.72 24.2267 8 21.64C8.04 18.9867 13.3333 17.5333 16 17.5333C18.6533 17.5333 23.96 18.9867 24 21.64C22.28 24.2267 19.3333 25.9333 16 25.9333Z"
fill="#3591FE"
/>
</svg>
);
};
export default MemberIcon;

View File

@@ -7,8 +7,6 @@ const PersonIcon: FC<IconProps> = ({
onClick,
...restProps
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
<svg
width="16"
@@ -17,13 +15,11 @@ const PersonIcon: FC<IconProps> = ({
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={onClick as MouseEventHandler<SVGSVGElement>}
onMouseEnter={() => setIsHovered(true)} // Set isHovered to true on hover
onMouseLeave={() => setIsHovered(false)}
style={{ cursor: 'pointer' }}
>
<path
d="M8 8C6.9 8 5.95833 7.60833 5.175 6.825C4.39167 6.04167 4 5.1 4 4C4 2.9 4.39167 1.95833 5.175 1.175C5.95833 0.391667 6.9 0 8 0C9.1 0 10.0417 0.391667 10.825 1.175C11.6083 1.95833 12 2.9 12 4C12 5.1 11.6083 6.04167 10.825 6.825C10.0417 7.60833 9.1 8 8 8ZM2 16C1.45 16 0.979333 15.8043 0.588 15.413C0.196 15.021 0 14.55 0 14V13.2C0 12.6333 0.146 12.1123 0.438 11.637C0.729334 11.1623 1.11667 10.8 1.6 10.55C2.63333 10.0333 3.68333 9.64567 4.75 9.387C5.81667 9.129 6.9 9 8 9C9.1 9 10.1833 9.129 11.25 9.387C12.3167 9.64567 13.3667 10.0333 14.4 10.55C14.8833 10.8 15.2707 11.1623 15.562 11.637C15.854 12.1123 16 12.6333 16 13.2V14C16 14.55 15.8043 15.021 15.413 15.413C15.021 15.8043 14.55 16 14 16H2Z"
fill={isHovered ? '#0276FE' : '#969696'}
fill={'#969696'}
/>
</svg>
);

View File

@@ -0,0 +1,33 @@
import React, { FC, MouseEventHandler, useState } from 'react';
import { IconProps } from './types';
const PersonIconFill: FC<IconProps> = ({
width = '32',
height = '32',
onClick,
...restProps
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
onClick={onClick as MouseEventHandler<SVGSVGElement>}
onMouseEnter={() => setIsHovered(true)} // Set isHovered to true on hover
onMouseLeave={() => setIsHovered(false)}
>
<rect width="32" height="32" rx="16" fill="#E8E8E8" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 7C18.2133 7 20 8.78667 20 11C20 13.2133 18.2133 15 16 15C13.7867 15 12 13.2133 12 11C12 8.78667 13.7867 7 16 7ZM16 25.9333C12.6667 25.9333 9.72 24.2267 8 21.64C8.04 18.9867 13.3333 17.5333 16 17.5333C18.6533 17.5333 23.96 18.9867 24 21.64C22.28 24.2267 19.3333 25.9333 16 25.9333Z"
fill="#585757"
/>
</svg>
);
};
export default PersonIconFill;

View File

@@ -0,0 +1,28 @@
import { FC, MouseEventHandler, useState } from 'react';
import { IconProps } from './types';
const PersonIcon: FC<IconProps> = ({
width = '24',
height = '24',
onClick,
...restProps
}) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={onClick as MouseEventHandler<SVGSVGElement>}
style={{ cursor: 'pointer' }}
>
<path
d="M8 8C6.9 8 5.95833 7.60833 5.175 6.825C4.39167 6.04167 4 5.1 4 4C4 2.9 4.39167 1.95833 5.175 1.175C5.95833 0.391667 6.9 0 8 0C9.1 0 10.0417 0.391667 10.825 1.175C11.6083 1.95833 12 2.9 12 4C12 5.1 11.6083 6.04167 10.825 6.825C10.0417 7.60833 9.1 8 8 8ZM2 16C1.45 16 0.979333 15.8043 0.588 15.413C0.196 15.021 0 14.55 0 14V13.2C0 12.6333 0.146 12.1123 0.438 11.637C0.729334 11.1623 1.11667 10.8 1.6 10.55C2.63333 10.0333 3.68333 9.64567 4.75 9.387C5.81667 9.129 6.9 9 8 9C9.1 9 10.1833 9.129 11.25 9.387C12.3167 9.64567 13.3667 10.0333 14.4 10.55C14.8833 10.8 15.2707 11.1623 15.562 11.637C15.854 12.1123 16 12.6333 16 13.2V14C16 14.55 15.8043 15.021 15.413 15.413C15.021 15.8043 14.55 16 14 16H2Z"
fill="#0276FE"
/>
</svg>
);
};
export default PersonIcon;

33
src/assets/TeamIcon.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { IconProps } from '@navi/web-ui/lib/icons/types';
import { FC } from 'react';
const TeamIcon: FC<IconProps> = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<mask
id="mask0_3268_31763"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="24"
height="24"
>
<rect width="24" height="24" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_3268_31763)">
<path
d="M4 13.525L6.525 11L4 8.475L1.475 11L4 13.525ZM17.5 13L20 9L22.5 13H17.5ZM0 18V16.425C0 15.6917 0.371 15.104 1.113 14.662C1.85433 14.2207 2.81667 14 4 14C4.21667 14 4.425 14.004 4.625 14.012C4.825 14.0207 5.01667 14.0417 5.2 14.075C4.96667 14.4083 4.79167 14.7667 4.675 15.15C4.55833 15.5333 4.5 15.9417 4.5 16.375V18H0ZM6 18V16.375C6 15.2917 6.55433 14.4167 7.663 13.75C8.771 13.0833 10.2167 12.75 12 12.75C13.8 12.75 15.25 13.0833 16.35 13.75C17.45 14.4167 18 15.2917 18 16.375V18H6ZM19.5 18V16.375C19.5 15.9417 19.4457 15.5333 19.337 15.15C19.229 14.7667 19.0667 14.4083 18.85 14.075C19.0333 14.0417 19.221 14.0207 19.413 14.012C19.6043 14.004 19.8 14 20 14C21.2 14 22.1667 14.2207 22.9 14.662C23.6333 15.104 24 15.6917 24 16.425V18H19.5ZM12 12C11.1667 12 10.4583 11.7083 9.875 11.125C9.29167 10.5417 9 9.83333 9 9C9 8.15 9.29167 7.43733 9.875 6.862C10.4583 6.28733 11.1667 6 12 6C12.85 6 13.5623 6.28733 14.137 6.862C14.7123 7.43733 15 8.15 15 9C15 9.83333 14.7123 10.5417 14.137 11.125C13.5623 11.7083 12.85 12 12 12Z"
fill="white"
/>
</g>
</svg>
);
};
export default TeamIcon;

View File

@@ -10,6 +10,7 @@ import LeadIcon from '@navi/web-ui/lib/icons/LeadIcon';
import { NavItemType } from '@navi/web-ui/lib/components/Navbar/types';
import { Typography } from '@navi/web-ui/lib/primitives';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import TeamIcon from '../../assets/TeamIcon';
import JiraDashboardIcon from '@src/assets/JiraDashboardIcon';
import Dialog from '../Dialog';
import styles from './LeftNav.module.scss';
@@ -58,7 +59,7 @@ const LeftNav: React.FC<LeftNavProps> = ({ children }) => {
itemType: 'simpleNavItem',
label: 'Team',
route: '/team',
Icon: LeadIcon,
Icon: TeamIcon,
handleNavigation: () => navigate('/team'),
},
{

View File

@@ -5,6 +5,7 @@ const Incident = lazy(() => import('./Pages/Incidents/index'));
const Team = lazy(() => import('./Pages/Team/index'));
const Severity = lazy(() => import('./Pages/Severity/index'));
const Tableau = lazy(() => import('./Pages/Tableau/index'));
const TeamRevamp = lazy(() => import('./Pages/TeamRevamp/index'));
const JiraDashboard = lazy(() => import('./Pages/JiraDashboard/index'));
import { CustomRouteObject } from './types';
@@ -19,11 +20,6 @@ const routes: CustomRouteObject[] = [
path: '/incident/:incidentId',
element: <Incident />,
},
{
id: 'TEAM',
path: '/team',
element: <Team />,
},
{
id: 'SEVERITY',
path: '/severity',
@@ -34,6 +30,11 @@ const routes: CustomRouteObject[] = [
path: '/metrics',
element: <Tableau />,
},
{
id: 'TEAM',
path: '/team',
element: <TeamRevamp />,
},
{
id: 'JIRA_DASHBOARD',
path: '/jiraDashboard',

View File

@@ -0,0 +1,10 @@
import { useRef } from 'react';
const useIsFirstRender = (): boolean => {
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
};
export default useIsFirstRender;

View File

@@ -0,0 +1,12 @@
import { useState, useEffect } from 'react';
const useAuthData = () => {
const [userRole, setUserRole] = useState<string>('');
useEffect(() => {
const userData = JSON.parse(localStorage.getItem('user-data') || '{}');
setUserRole(userData?.roles || []);
}, []);
return userRole;
};
export { useAuthData };

119
src/slices/team1Slice.tsx Normal file
View File

@@ -0,0 +1,119 @@
import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import {
FETCH_SINGLE_TEAM_DATA,
FETCH_TEAM_DATA,
} from '@src/Pages/TeamRevamp/constants';
import { TeamsData, TeamsDetail } from '@src/Pages/TeamRevamp/types';
import { ApiService } from '@src/services/api';
import { RootState } from '@src/store';
export interface TeamState {
data: TeamsData;
isPending: boolean;
modalOpen: boolean;
teamDetails: TeamsDetail;
error: string;
selectedTeam: TeamsDetail;
newTeam: number | string;
}
type TeamInitialState = {
teams: TeamState;
};
const initialState: TeamInitialState = {
teams: {
data: {},
error: '',
isPending: false,
modalOpen: false,
teamDetails: {},
selectedTeam: {},
newTeam: '',
},
};
export const fetchTeams = createAsyncThunk('team/fetchTeams', async () => {
const endPoint = FETCH_TEAM_DATA;
return await ApiService.get(endPoint);
});
export const fetchTeamDetails = createAsyncThunk(
'team/fetchTeamDetails',
async (teamId: string) => {
const endPoint = FETCH_SINGLE_TEAM_DATA(teamId);
return await ApiService.get(endPoint);
},
);
const team1Slice = createSlice({
name: 'team1',
initialState,
reducers: {
setTeamData: (state, action: PayloadAction<TeamsData>) => {
state.teams.data = action.payload;
},
setModalOpen: (state, action: PayloadAction<boolean>) => {
state.teams.modalOpen = action.payload;
},
setTeamDetails: (state, action: PayloadAction<TeamsDetail>) => {
state.teams.teamDetails = action.payload;
},
setNewTeam: (state, action: PayloadAction<string>) => {
state.teams.newTeam = action.payload;
},
setSelectedTeam: (state, action: PayloadAction<TeamsDetail>) => {
state.teams.selectedTeam = action.payload;
},
},
extraReducers: builder => {
builder.addCase(fetchTeams.fulfilled, (state, action) => {
state.teams.data = action.payload.data.data;
state.teams.isPending = false;
state.teams.error = '';
if (!state.teams.selectedTeam) {
state.teams.selectedTeam = action.payload.data.data[0];
}
});
builder.addCase(fetchTeams.rejected, (state, action) => {
state.teams.error = action.error.message ?? 'Something went wrong';
state.teams.data = {};
state.teams.isPending = false;
state.teams.selectedTeam = {};
});
builder.addCase(fetchTeams.pending, (state, action) => {
state.teams.isPending = true;
state.teams.data = {};
state.teams.error = '';
state.teams.selectedTeam = {};
});
builder.addCase(fetchTeamDetails.fulfilled, (state, action) => {
state.teams.teamDetails = action.payload.data.data;
state.teams.isPending = false;
state.teams.error = '';
});
builder.addCase(fetchTeamDetails.rejected, (state, action) => {
state.teams.error = action.error.message ?? 'Something went wrong';
state.teams.teamDetails = {};
state.teams.isPending = false;
});
builder.addCase(fetchTeamDetails.pending, (state, action) => {
state.teams.isPending = true;
state.teams.teamDetails = {};
state.teams.error = '';
});
},
});
export const selectSearchTeamData = (state: RootState) => state.team1.teams;
export const {
setTeamData,
setModalOpen,
setTeamDetails,
setSelectedTeam,
setNewTeam,
} = team1Slice.actions;
export default team1Slice.reducer;

View File

@@ -2,12 +2,17 @@ import { configureStore } from '@reduxjs/toolkit';
import teamReducer from '../slices/teamSlice';
import severityReducer from '../slices/sevSlice';
import dashboardReducer from '../slices/dashboardSlice';
import teamReducer1 from '../slices/team1Slice';
import jiraDashboardReducer from '../slices/jiraDashboardSlice';
const store = configureStore({
reducer: {
team: teamReducer,
severity: severityReducer,
dashboard: dashboardReducer,
team1: teamReducer1,
jiraDashboard: jiraDashboardReducer,
},
});