diff --git a/package.json b/package.json index 8af7131..d3db39b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Pages/Incidents/Incidents.module.scss b/src/Pages/Incidents/Incidents.module.scss index 309e996..5e938cb 100644 --- a/src/Pages/Incidents/Incidents.module.scss +++ b/src/Pages/Incidents/Incidents.module.scss @@ -184,6 +184,9 @@ display: inline; } +.list-wrapper { + display: flex; +} .hint-text { margin-top: 24px; font-size: 12px; diff --git a/src/Pages/TeamRevamp/Team.module.scss b/src/Pages/TeamRevamp/Team.module.scss new file mode 100644 index 0000000..a703eea --- /dev/null +++ b/src/Pages/TeamRevamp/Team.module.scss @@ -0,0 +1,7 @@ +.team-wrapper { + margin: 24px; +} + +.wrapper-class { + display: flex; +} diff --git a/src/Pages/TeamRevamp/TeamDetails/TeamDetails.module.scss b/src/Pages/TeamRevamp/TeamDetails/TeamDetails.module.scss new file mode 100644 index 0000000..5f8e897 --- /dev/null +++ b/src/Pages/TeamRevamp/TeamDetails/TeamDetails.module.scss @@ -0,0 +1,7 @@ +.team-details-wrapper { + margin: 20px 0px 0px 79px; +} +.fallback-component { + height: 100px; + margin-left: 450px; +} diff --git a/src/Pages/TeamRevamp/TeamDetails/index.tsx b/src/Pages/TeamRevamp/TeamDetails/index.tsx new file mode 100644 index 0000000..50ed116 --- /dev/null +++ b/src/Pages/TeamRevamp/TeamDetails/index.tsx @@ -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 ( +
+ +
+ ); + } + return ( +
+ + +
+ ); +}; + +export default TeamDetails; diff --git a/src/Pages/TeamRevamp/TeamList/SearchInput.tsx b/src/Pages/TeamRevamp/TeamList/SearchInput.tsx new file mode 100644 index 0000000..1bf991e --- /dev/null +++ b/src/Pages/TeamRevamp/TeamList/SearchInput.tsx @@ -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 = ({ + value, + onChange, + isAdmin, +}) => { + const containerClass = isAdmin + ? styles['search-bar'] + : styles['search-input']; + return ( + } + onChange={onChange} + containerClassName={containerClass} + value={value} + placeholder="Search by team " + /> + ); +}; + +export default SearchInput; diff --git a/src/Pages/TeamRevamp/TeamList/TeamList.module.scss b/src/Pages/TeamRevamp/TeamList/TeamList.module.scss new file mode 100644 index 0000000..d81b64f --- /dev/null +++ b/src/Pages/TeamRevamp/TeamList/TeamList.module.scss @@ -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; +} diff --git a/src/Pages/TeamRevamp/TeamList/index.tsx b/src/Pages/TeamRevamp/TeamList/index.tsx new file mode 100644 index 0000000..07d2a1d --- /dev/null +++ b/src/Pages/TeamRevamp/TeamList/index.tsx @@ -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 ( +
+
{option.label}
+
+
{option.additionalData.count}
+
+ {option.additionalData.isSelected ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +const TeamList: React.FC = () => { + const dispatch = useDispatch(); + 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(); + const newTeam = useSelector((state: RootState) => state.team1.teams.newTeam); + const [input, setInput] = useState(''); + const scroll = useRef(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, + ): 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 ( +
+
+
+ + {Role.includes('Admin') && ( + + )} +
+ {input && ( +
+ {' '} + + {' '} + SEARCH RESULTS{' '} + +
+ )} + {returnIsFilteredResultEmpty() ? ( +
+ {' '} + + {' '} + No results found + +
+ ) : null} + +
+ +
+ ); +}; + +export default TeamList; diff --git a/src/Pages/TeamRevamp/constants.ts b/src/Pages/TeamRevamp/constants.ts new file mode 100644 index 0000000..2e7f89f --- /dev/null +++ b/src/Pages/TeamRevamp/constants.ts @@ -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; + } +}; diff --git a/src/Pages/TeamRevamp/index.tsx b/src/Pages/TeamRevamp/index.tsx new file mode 100644 index 0000000..6941f14 --- /dev/null +++ b/src/Pages/TeamRevamp/index.tsx @@ -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(); + 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 ( + +
+ Teams +
+ + +
+
+
+ ); +}; + +export default TeamRevamp; diff --git a/src/Pages/TeamRevamp/partials/Bots.tsx b/src/Pages/TeamRevamp/partials/Bots.tsx new file mode 100644 index 0000000..4f39381 --- /dev/null +++ b/src/Pages/TeamRevamp/partials/Bots.tsx @@ -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 = []; + +export const getBots = (): Array => 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); + }); +}; diff --git a/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.module.scss b/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.module.scss new file mode 100644 index 0000000..fb08d8f --- /dev/null +++ b/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.module.scss @@ -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; +} diff --git a/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.tsx b/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.tsx new file mode 100644 index 0000000..ae1fcf0 --- /dev/null +++ b/src/Pages/TeamRevamp/partials/CreateTeam/CreateTeam.tsx @@ -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 = ({ setTeam }) => { + const dispatch = useDispatch(); + 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, + ): void => { + const inputValue = e.target.value; + dispatchData({ + type: actionTypes.SET_TEAM_NAME, + payload: inputValue, + }); + validateTeamName(inputValue); + }; + + const handleEmailChange = (e: React.ChangeEvent): 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 ( +
+ {modalOpen && ( + +
+ + } + placeholder="E.g. NaviPay_Operations" + /> + +
+ +
+ + } + /> + +
+
+ )} +
+ ); +}; +export default CreateTeam; diff --git a/src/Pages/TeamRevamp/partials/SlackDetails/Slack.module.scss b/src/Pages/TeamRevamp/partials/SlackDetails/Slack.module.scss new file mode 100644 index 0000000..5f8e24c --- /dev/null +++ b/src/Pages/TeamRevamp/partials/SlackDetails/Slack.module.scss @@ -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; +} diff --git a/src/Pages/TeamRevamp/partials/SlackDetails/index.tsx b/src/Pages/TeamRevamp/partials/SlackDetails/index.tsx new file mode 100644 index 0000000..4be58a8 --- /dev/null +++ b/src/Pages/TeamRevamp/partials/SlackDetails/index.tsx @@ -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(null); + + useEffect(() => { + dispatchData({ + type: actionTypes.SET_INPUT, + payload: '', + }); + dispatchData({ + type: actionTypes.SET_PSEC_INPUT, + payload: '', + }); + }, [teamData]); + + const handleslackChannelId = ( + e: React.ChangeEvent, + ): 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; + + const refPsecOncall = useOutsideClick({ + callback: handlePsecOncallOutsideClick, + }) as MutableRefObject; + + 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): void => { + dispatchData({ + type: actionTypes.SET_INPUT, + payload: e.target.value, + }); + }; + const handlePsecInputChange = ( + e: React.ChangeEvent, + ): 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 ( +
+ + {teamDetails?.name} + {' '} + + SLACK DETAILS + {' '} +
+
+
+ + Team channel + +
+ + + +
+
+ +
+
+
+ + Team on call + {' '} +
+ +
+ {state.openOnCall && ( + + )} +
+
+ + PSEC on call + +
+ +
+ {state.openPsecOnCall && ( + + )} +
+ +
+
+
+
+
+ ); +}; + +export default TeamDetails; diff --git a/src/Pages/TeamRevamp/partials/TeamMemberDetails/TeamMemberDetails.module.scss b/src/Pages/TeamRevamp/partials/TeamMemberDetails/TeamMemberDetails.module.scss new file mode 100644 index 0000000..f6112f3 --- /dev/null +++ b/src/Pages/TeamRevamp/partials/TeamMemberDetails/TeamMemberDetails.module.scss @@ -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); +} diff --git a/src/Pages/TeamRevamp/partials/TeamMemberDetails/index.tsx b/src/Pages/TeamRevamp/partials/TeamMemberDetails/index.tsx new file mode 100644 index 0000000..edc2e0e --- /dev/null +++ b/src/Pages/TeamRevamp/partials/TeamMemberDetails/index.tsx @@ -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): 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 ( +
+
+
+ + MEMBERS + +
{' '} +
+ {(isUserParticipant(teamDetails) || + Role.includes('Admin') || + userEmail === managerEmail) && ( + + )} +
+
+
+ {teamMembers?.map(item => ( +
handleHoverIcon(item)} + onMouseLeave={() => handleHoverchange(item)} + > +
+
+ {manager?.name === item.name ? ( + + ) : ( +
+ {state.hovered.ishovered && + state.hovered.id === item.id ? ( + + ) : ( + + )} +
+ )} +
+ +
+ {' '} + {item.name} + {item.email} + + {manager?.name === item.name ? 'Manager' : ''} + {' '} +
+ {manager?.name !== item.name && ( + + )} +
+
+ {manager?.name !== item.name && ( +
{ + handleOpenDialog(item); + }} + className={styles['delete-icon']} + > + +
+ )} +
+
+ ))} +
+ { + makeManager(state.selectedMember?.id.toString() || ''); + handleResetModal(); + }, + }, + ]} + header="Update manager? " + onClose={handleResetModal} + > +
+ + Are you sure you want to make {`${state.selectedMember?.name}`}{' '} + manager of this team? + +
+
+ +
+ + Are you sure you want to remove {`${state.selectedMember?.name}`}{' '} + from this team? + +
+ +
+ + +
+
+
+
+ ); +}; +export default TeamMemberDetails; diff --git a/src/Pages/TeamRevamp/types.ts b/src/Pages/TeamRevamp/types.ts new file mode 100644 index 0000000..47cebde --- /dev/null +++ b/src/Pages/TeamRevamp/types.ts @@ -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) => 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; + lastUpdatedAt?: string; + expand?: boolean; +} +export interface CreateTeamProps { + setTeam: React.Dispatch< + React.SetStateAction + >; +} +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; + 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; +} diff --git a/src/Pages/TeamRevamp/useTeamApis.tsx b/src/Pages/TeamRevamp/useTeamApis.tsx new file mode 100644 index 0000000..90e56bf --- /dev/null +++ b/src/Pages/TeamRevamp/useTeamApis.tsx @@ -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(); + 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; diff --git a/src/Pages/TeamRevamp/util.ts b/src/Pages/TeamRevamp/util.ts new file mode 100644 index 0000000..e248c44 --- /dev/null +++ b/src/Pages/TeamRevamp/util.ts @@ -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; diff --git a/src/assets/AlertIcon.tsx b/src/assets/AlertIcon.tsx new file mode 100644 index 0000000..71e325c --- /dev/null +++ b/src/assets/AlertIcon.tsx @@ -0,0 +1,38 @@ +import React, { FC, useState } from 'react'; +import { IconProps } from './types'; + +const AlertIcon: FC = ({ + width = '24', + height = '24', + onClick, + ...restProps +}) => { + return ( + + + + + + + + + ); +}; + +export default AlertIcon; diff --git a/src/assets/DeleteIcon.tsx b/src/assets/DeleteIcon.tsx new file mode 100644 index 0000000..8684cc8 --- /dev/null +++ b/src/assets/DeleteIcon.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react'; +import { IconProps } from './types'; + +const DeleteIcon: FC = ({ + width = '24', + height = '24', + onClick, + ...restProps +}) => { + return ( + + + + ); +}; + +export default DeleteIcon; diff --git a/src/assets/FilterIcon.tsx b/src/assets/FilterIcon.tsx index 9d066f5..0f11913 100644 --- a/src/assets/FilterIcon.tsx +++ b/src/assets/FilterIcon.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import { FC } from 'react'; import { IconProps } from './types'; diff --git a/src/assets/ManagerIconFill.tsx b/src/assets/ManagerIconFill.tsx new file mode 100644 index 0000000..b594c29 --- /dev/null +++ b/src/assets/ManagerIconFill.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { IconProps } from './types'; + +const ManagerIconFill: FC = ({ + width = '32', + height = '32', + ...restProps +}) => { + return ( + + + + + ); +}; + +export default ManagerIconFill; diff --git a/src/assets/MemberIcon.tsx b/src/assets/MemberIcon.tsx new file mode 100644 index 0000000..5dbb4be --- /dev/null +++ b/src/assets/MemberIcon.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { IconProps } from './types'; + +const MemberIcon: FC = ({ + width = '32', + height = '32', + ...restProps +}) => { + return ( + + + + + ); +}; + +export default MemberIcon; diff --git a/src/assets/PersonIcon.tsx b/src/assets/PersonIcon.tsx index 352a268..343ca4e 100644 --- a/src/assets/PersonIcon.tsx +++ b/src/assets/PersonIcon.tsx @@ -7,8 +7,6 @@ const PersonIcon: FC = ({ onClick, ...restProps }) => { - const [isHovered, setIsHovered] = useState(false); - return ( = ({ fill="none" xmlns="http://www.w3.org/2000/svg" onClick={onClick as MouseEventHandler} - onMouseEnter={() => setIsHovered(true)} // Set isHovered to true on hover - onMouseLeave={() => setIsHovered(false)} style={{ cursor: 'pointer' }} > ); diff --git a/src/assets/PersonIconFill.tsx b/src/assets/PersonIconFill.tsx new file mode 100644 index 0000000..1e22078 --- /dev/null +++ b/src/assets/PersonIconFill.tsx @@ -0,0 +1,33 @@ +import React, { FC, MouseEventHandler, useState } from 'react'; +import { IconProps } from './types'; + +const PersonIconFill: FC = ({ + width = '32', + height = '32', + onClick, + ...restProps +}) => { + const [isHovered, setIsHovered] = useState(false); + return ( + } + onMouseEnter={() => setIsHovered(true)} // Set isHovered to true on hover + onMouseLeave={() => setIsHovered(false)} + > + + + + ); +}; + +export default PersonIconFill; diff --git a/src/assets/SelectedPersonIcon.tsx b/src/assets/SelectedPersonIcon.tsx new file mode 100644 index 0000000..1b4d066 --- /dev/null +++ b/src/assets/SelectedPersonIcon.tsx @@ -0,0 +1,28 @@ +import { FC, MouseEventHandler, useState } from 'react'; +import { IconProps } from './types'; + +const PersonIcon: FC = ({ + width = '24', + height = '24', + onClick, + ...restProps +}) => { + return ( + } + style={{ cursor: 'pointer' }} + > + + + ); +}; + +export default PersonIcon; diff --git a/src/assets/TeamIcon.tsx b/src/assets/TeamIcon.tsx new file mode 100644 index 0000000..7907d0d --- /dev/null +++ b/src/assets/TeamIcon.tsx @@ -0,0 +1,33 @@ +import { IconProps } from '@navi/web-ui/lib/icons/types'; +import { FC } from 'react'; + +const TeamIcon: FC = () => { + return ( + + + + + + + + + ); +}; + +export default TeamIcon; diff --git a/src/components/LeftNav/index.tsx b/src/components/LeftNav/index.tsx index 840cad3..0ee533d 100644 --- a/src/components/LeftNav/index.tsx +++ b/src/components/LeftNav/index.tsx @@ -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 = ({ children }) => { itemType: 'simpleNavItem', label: 'Team', route: '/team', - Icon: LeadIcon, + Icon: TeamIcon, handleNavigation: () => navigate('/team'), }, { diff --git a/src/router.tsx b/src/router.tsx index f1c1761..dd6c79e 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -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: , }, - { - id: 'TEAM', - path: '/team', - element: , - }, { id: 'SEVERITY', path: '/severity', @@ -34,6 +30,11 @@ const routes: CustomRouteObject[] = [ path: '/metrics', element: , }, + { + id: 'TEAM', + path: '/team', + element: , + }, { id: 'JIRA_DASHBOARD', path: '/jiraDashboard', diff --git a/src/services/hooks/isFirstRender.tsx b/src/services/hooks/isFirstRender.tsx new file mode 100644 index 0000000..2fdb81e --- /dev/null +++ b/src/services/hooks/isFirstRender.tsx @@ -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; diff --git a/src/services/hooks/useAuth.tsx b/src/services/hooks/useAuth.tsx new file mode 100644 index 0000000..0ff1b17 --- /dev/null +++ b/src/services/hooks/useAuth.tsx @@ -0,0 +1,12 @@ +import { useState, useEffect } from 'react'; +const useAuthData = () => { + const [userRole, setUserRole] = useState(''); + + useEffect(() => { + const userData = JSON.parse(localStorage.getItem('user-data') || '{}'); + setUserRole(userData?.roles || []); + }, []); + + return userRole; +}; +export { useAuthData }; diff --git a/src/slices/team1Slice.tsx b/src/slices/team1Slice.tsx new file mode 100644 index 0000000..a28dc06 --- /dev/null +++ b/src/slices/team1Slice.tsx @@ -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) => { + state.teams.data = action.payload; + }, + setModalOpen: (state, action: PayloadAction) => { + state.teams.modalOpen = action.payload; + }, + setTeamDetails: (state, action: PayloadAction) => { + state.teams.teamDetails = action.payload; + }, + setNewTeam: (state, action: PayloadAction) => { + state.teams.newTeam = action.payload; + }, + setSelectedTeam: (state, action: PayloadAction) => { + 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; diff --git a/src/store/index.tsx b/src/store/index.tsx index 96c692b..f226248 100644 --- a/src/store/index.tsx +++ b/src/store/index.tsx @@ -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, }, });