// <copyright file="discover-teams-wrapper-page.tsx" company="Microsoft"> // Copyright (c) Microsoft. All rights reserved. // </copyright> import * as React from "react"; import { Loader} from "@fluentui/react-northstar"; import Card from "./card"; import NoPostAddedPage from "./no-post-added-page"; import FilterNoPostContentPage from "./filter-no-post-content-page"; import TitleBar from "../filter-bar-teams/title-bar-teams"; import { Container, Col, Row } from "react-bootstrap"; import * as microsoftTeams from "@microsoft/teams-js"; import { IProjectDetails } from "./discover-wrapper-page"; import { getTeamAllProjects, getFilteredTeamProjects, filterTitleAndSkillsTeam } from "../../api/discover-api"; import { getConfigSkills } from "../../api/teams-config-tab-api"; import { generateColor } from "../../helpers/helper"; import NotificationMessage from "../notification-message/notification-message"; import { WithTranslation, withTranslation } from "react-i18next"; import { TFunction } from "i18next"; import { ICheckBoxItem } from "../filter-bar/filter-bar"; import Resources from "../../constants/resources"; import InfiniteScroll from 'react-infinite-scroller'; import 'bootstrap/dist/css/bootstrap.min.css'; import "../../styles/site.css"; import "../../styles/card.css"; export interface IUserVote { projectId: string; userId: string; } interface ICardViewState { loader: boolean; resourceStrings: any; projectDetails: Array<IProjectDetails>; projectSearchDetails: Array<IProjectDetails>; alertMessage: string; alertprojectStatus: number; showAlert: boolean; searchText: string; showNoProjectPage: boolean; infiniteScrollParentKey: number; isFilterApplied: boolean; isPageInitialLoad: boolean; pageLoadStart: number; hasMoreProjects: boolean; skillList: Array<string>, initialProjects: Array<IProjectDetails>; } class DiscoverWrapperPage extends React.Component<WithTranslation, ICardViewState> { localize: TFunction; selectedSharedBy: Array<ICheckBoxItem>; selectedPostprojectStatus: Array<ICheckBoxItem>; selectedskills: Array<ICheckBoxItem>; selectedSortBy: string; filterSearchText: string; allProjects: Array<IProjectDetails>; loggedInUserObjectId: string; loggedInUserName: string; teamId: string; authorAvatarBackground: Array<any>; hasMoreProjects: boolean; constructor(props: any) { super(props); let colors = localStorage.getItem("avatar-colors"); this.localize = this.props.t; this.selectedSharedBy = []; this.selectedPostprojectStatus = []; this.selectedskills = []; this.selectedSortBy = ""; this.filterSearchText = ""; this.allProjects = []; this.loggedInUserObjectId = ""; this.loggedInUserName= ""; this.teamId = ""; this.authorAvatarBackground = colors === null ? [] : JSON.parse(colors!); this.hasMoreProjects = true; this.state = { loader: true, projectDetails: [], projectSearchDetails: [], resourceStrings: {}, alertMessage: "", alertprojectStatus: 0, showAlert: false, searchText: "", showNoProjectPage: false, isFilterApplied: false, infiniteScrollParentKey: 0, isPageInitialLoad: true, pageLoadStart: -1, hasMoreProjects: true, initialProjects: [], skillList: [], } } /** * Used to initialize Microsoft Teams sdk */ async componentDidMount() { microsoftTeams.initialize(); microsoftTeams.getContext((context: microsoftTeams.Context) => { this.setState({ loader: true }); this.teamId = context.teamId!; this.loggedInUserName = context.userPrincipalName!; this.loggedInUserObjectId = context.userObjectId!; this.getConfigSkills(); this.initprojectDetails(); }); } /** * Get skills configured for a team. */ getConfigSkills = async () => { let response = await getConfigSkills(this.teamId); if (response.status === 200 && response.data) { this.setState({ skillList: response.data.skills }); } } /** * Fetch projects for initializing grid */ initprojectDetails = async () => { let response = await getTeamAllProjects(this.teamId, 0); if (response.status === 200 && response.data) { this.setState({ initialProjects: response.data, loader: false }); } } /** * Get comma separated selected filter entities string. * @param filterEntity Array of selected filter entities. */ private getFilterString(filterEntity: Array<string>) { return filterEntity.length > 1 ? filterEntity.join(";") : filterEntity.length === 1 ? filterEntity.join(";") + ";" : ""; } /** * Get filtered projects based on selected checkboxes. * @param pageCount Page count for which next set of projects needs to be fetched */ getFilteredprojectDetails = async (pageCount: number) => { let postprojectStatuss = this.selectedPostprojectStatus.map((postprojectStatus: ICheckBoxItem) => { return postprojectStatus.key.toString().trim() }); let postprojectStatussString = encodeURI(this.getFilterString(postprojectStatuss)); let authors = this.selectedSharedBy.map((authors: ICheckBoxItem) => { return authors.title.trim() }); let authorsString = encodeURI(this.getFilterString(authors)); let skills = this.selectedskills.map((skill: ICheckBoxItem) => { return skill.title.trim() }); let skillsString = encodeURI(this.getFilterString(skills)); let response = await getFilteredTeamProjects(postprojectStatussString, authorsString, skillsString, this.teamId, pageCount); if (response.status === 200 && response.data) { if (response.data.length < 50) { this.hasMoreProjects = false; } else { this.hasMoreProjects = true; } response.data.map((post: IProjectDetails) => { let searchedAuthor = this.authorAvatarBackground.find((author) => author.id === post.createdByUserId); if (searchedAuthor) { post.avatarBackgroundColor = searchedAuthor.color; } else { let color = generateColor(); this.authorAvatarBackground.push({ id: post.createdByUserId, color: color }); post.avatarBackgroundColor = color; localStorage.setItem("avatar-colors", JSON.stringify(this.authorAvatarBackground)); } if (post.createdByUserId === this.loggedInUserObjectId) { post.isCurrentUserProject = true; } else { post.isCurrentUserProject = false; } this.allProjects.push(post); }); if (response.data.count !== 0) { this.setState({ isPageInitialLoad: false, }); } else { this.setState({ showNoProjectPage: true, isPageInitialLoad: false }) } //this.getUserVotes(); this.onFilterSearchTextChange(this.filterSearchText); } } /** * Reset app user selected filters */ resetAllFilters = () => { this.selectedSortBy = Resources.sortBy[0].id; this.selectedSharedBy = []; this.selectedPostprojectStatus = []; this.selectedskills = []; this.filterSearchText = ""; } /** * Fetch projects for Team tab from API * @param pageCount Page count for which next set of projects needs to be fetched */ getprojectDetails = async (pageCount: number) => { this.resetAllFilters(); let response = await getTeamAllProjects(this.teamId, pageCount); if (response.status === 200 && response.data) { if (response.data.length < 50) { this.hasMoreProjects = false; } else { this.hasMoreProjects = true; } response.data.map((post: IProjectDetails) => { let searchedAuthor = this.authorAvatarBackground.find((author) => author.id === post.createdByUserId); if (searchedAuthor) { post.avatarBackgroundColor = searchedAuthor.color; } else { let color = generateColor(); this.authorAvatarBackground.push({ id: post.createdByUserId, color: color }); post.avatarBackgroundColor = color; localStorage.setItem("avatar-colors", JSON.stringify(this.authorAvatarBackground)); } if (post.createdByUserId === this.loggedInUserObjectId) { post.isCurrentUserProject = true; } else { post.isCurrentUserProject = false; } this.allProjects.push(post); }); if (response.data.count === 0) { this.setState({ showNoProjectPage: true }) } //this.getUserVotes(); this.onFilterSearchTextChange(this.filterSearchText); } this.setState({ searchText: "", isPageInitialLoad: false }); } /** *Sets state for showing alert notification. *@param content Notification message *@param projectStatus Boolean value indicating 1- Success 2- Error */ showAlert = (content: string, projectStatus: number) => { this.setState({ alertMessage: content, alertprojectStatus: projectStatus, showAlert: true }, () => { setTimeout(() => { this.setState({ showAlert: false }) }, 4000); }); } /** *Sets state for hiding alert notification. */ hideAlert = () => { this.setState({ showAlert: false }) } /** *Removes selected blog post from page *@param projectId Id of post which needs to be deleted *@param isSuccess Boolean indication whether operation succeeded */ handleDeleteButtonClick = (projectId: string, isSuccess: boolean) => { if (isSuccess) { this.allProjects.map((post: IProjectDetails) => { if (post.projectId === projectId) { post.isRemoved = true; } }); this.showAlert(this.localize("projectDeletedSuccess"), 1); this.onFilterSearchTextChange(this.filterSearchText); } else { this.showAlert(this.localize("postDeletedError"), 2); } } /** *Removes selected project from joined projects *@param projectId Id of project which needs to be deleted *@param isSuccess Boolean indication whether operation succeeded */ handleLeaveButtonClick = (projectId: string, isSuccess: boolean) => { if (isSuccess) { this.allProjects.map((post: IProjectDetails) => { if (post.projectId === projectId) { post.isRemoved = true; } }); this.showAlert(this.localize("leaveProjectSuccess"), 1); this.onFilterSearchTextChange(this.filterSearchText); } else { this.showAlert(this.localize("leaveProjectError"), 2); } } /** *Invoked by Infinite scroll component when user scrolls down to fetch next set of projects *@param pageCount Page count for which next set of projects needs to be fetched */ loadMoreProjects = (pageCount: number) => { if (!this.filterSearchText.trim().length) { if (this.state.searchText.trim().length) { this.searchFilterPostUsingAPI(pageCount); } else if (this.state.isFilterApplied) { this.getFilteredprojectDetails(pageCount); } else { this.getprojectDetails(pageCount); } } } /** *Set state of search text as per user input change *@param searchText Search text entered by user */ handleSearchInputChange = async (searchText: string) => { this.setState({ searchText: searchText }); if (searchText.length === 0) { this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], hasMoreProjects: true }); this.allProjects = []; } } /** *Filter cards based on user input after clicking search icon in search bar. */ searchFilterPostUsingAPI = async (pageCount: number) => { this.resetAllFilters(); if (this.state.searchText.trim().length) { let response = await filterTitleAndSkillsTeam(this.state.searchText, this.teamId, pageCount); if (response.status === 200 && response.data) { if (response.data.length < 50) { this.hasMoreProjects = false; } else { this.hasMoreProjects = true; } response.data.map((post: IProjectDetails) => { let searchedAuthor = this.authorAvatarBackground.find((author) => author.id === post.createdByUserId); if (searchedAuthor) { post.avatarBackgroundColor = searchedAuthor.color; } else { let color = generateColor(); this.authorAvatarBackground.push({ id: post.createdByUserId, color: color }); post.avatarBackgroundColor = color; localStorage.setItem("avatar-colors", JSON.stringify(this.authorAvatarBackground)); } if (post.createdByUserId === this.loggedInUserObjectId) { post.isCurrentUserProject = true; } else { post.isCurrentUserProject = false; } this.allProjects.push(post) }); this.setState({ isPageInitialLoad: false }); //this.getUserVotes(); this.onFilterSearchTextChange(this.filterSearchText); } } } /** *Filter cards based on 'shared by' checkbox selection. *@param selectedCheckboxes User selected checkbox array */ onSharedByCheckboxStateChange = (selectedCheckboxes: Array<ICheckBoxItem>) => { this.selectedSharedBy = selectedCheckboxes.filter((value) => { return value.isChecked }); this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], searchText: "", hasMoreProjects: true }); this.allProjects = []; } /** *Filter cards based on post projectStatus checkbox selection. *@param selectedCheckboxes User selected checkbox array */ onprojectStatusCheckboxStateChange = (selectedCheckboxes: Array<ICheckBoxItem>) => { this.selectedPostprojectStatus = selectedCheckboxes.filter((value) => { return value.isChecked }); this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], searchText: "", hasMoreProjects: true }); this.allProjects = []; } /** *Filter cards based on skills checkbox selection. *@param selectedCheckboxes User selected checkbox array */ onskillsStateChange = (selectedCheckboxes: Array<ICheckBoxItem>) => { this.selectedskills = selectedCheckboxes.filter((value) => { return value.isChecked }); this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], searchText: "", hasMoreProjects: true }); this.allProjects = []; } /** *Filter cards based sort by value. *@param selectedValue Selected value for 'sort by' */ onSortByChange = (selectedValue: string) => { this.selectedSortBy = selectedValue; this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], searchText: "", hasMoreProjects: true }); this.allProjects = []; } /** * Invoked when post is edited. Updates state and shows notification alert. * @param cardDetails Updated post details * @param isSuccess Boolean indicating whether edit operation is successful. */ onCardUpdate = (cardDetails: IProjectDetails, isSuccess: boolean) => { if (isSuccess) { this.allProjects.map((post: IProjectDetails) => { if (post.projectId === cardDetails.projectId) { post.description = cardDetails.description; post.title = cardDetails.title; post.requiredSkills = cardDetails.requiredSkills; post.status = cardDetails.status; post.projectParticipantsUserIds = cardDetails.projectParticipantsUserIds; post.projectParticipantsUserMapping = cardDetails.projectParticipantsUserMapping; post.projectEndDate = cardDetails.projectEndDate; post.projectStartDate = cardDetails.projectStartDate; post.teamSize = cardDetails.teamSize; post.supportDocuments = cardDetails.supportDocuments; } }); this.onFilterSearchTextChange(this.filterSearchText); this.showAlert(this.localize("postUpdateSuccess"), 1) } else { this.showAlert(this.localize("postUpdateError"), 2) } } /** * Invoked when new post is added. Shows notification alert. * @param isSuccess Boolean indicating whether add new post operation is successful. * @param getSubmittedPost Post details which needs to be added. */ onNewPost = (isSuccess: boolean, getSubmittedPost: IProjectDetails) => { if (isSuccess) { let searchedAuthor = this.authorAvatarBackground.find((author) => author.id === getSubmittedPost.createdByUserId); if (searchedAuthor) { getSubmittedPost.avatarBackgroundColor = searchedAuthor.color; } else { let color = generateColor(); this.authorAvatarBackground.push({ id: getSubmittedPost.createdByUserId, color: color }); getSubmittedPost.avatarBackgroundColor = color; localStorage.setItem("avatar-colors", JSON.stringify(this.authorAvatarBackground)); } let submittedPost = this.state.projectDetails; if (getSubmittedPost.createdByUserId === this.loggedInUserObjectId) { getSubmittedPost.isCurrentUserProject = true; } else { getSubmittedPost.isCurrentUserProject = false; } submittedPost.unshift(getSubmittedPost); this.setState({ projectDetails: submittedPost, initialProjects: submittedPost }); this.allProjects = this.state.projectDetails; this.showAlert(this.localize("addNewPostSuccess"), 1) } else { this.showAlert(this.localize("addNewPostError"), 2) } } /** * Filters projects inline by user search text * @param searchText Search text entered by user. */ onFilterSearchTextChange = (searchText: string) => { this.filterSearchText = searchText; if (searchText.trim().length) { let filteredPosts = this.allProjects.filter((post: IProjectDetails) => post.title.toLowerCase().includes(searchText.toLowerCase()) === true); this.setState({ projectDetails: filteredPosts, loader: false, hasMoreProjects: this.hasMoreProjects, isPageInitialLoad: false }); } else { this.setState({ projectDetails: [...this.allProjects], loader: false, hasMoreProjects: this.hasMoreProjects, isPageInitialLoad: false }); } } /** * Invoked when either filter bar is displayed or closed * @param isOpen Boolean indicating whether filter bar is displayed or closed. */ handleFilterClear = (isOpen: boolean) => { if (!isOpen && (this.selectedPostprojectStatus.length > 0 || this.selectedSharedBy.length > 0 || this.selectedskills.length > 0 || this.selectedSortBy !== Resources.sortBy[0].id)) { this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], searchText: "", hasMoreProjects: true }); this.allProjects = []; } this.setState({ isFilterApplied: isOpen }); this.resetAllFilters(); } /** * Invoked when user hits enter or clicks on search icon for searching post through command bar */ invokeApiSearch = () => { this.setState({ isPageInitialLoad: true, pageLoadStart: -1, infiniteScrollParentKey: this.state.infiniteScrollParentKey + 1, projectDetails: [], isFilterApplied: false, hasMoreProjects: true }); this.allProjects = []; } hideFilterbar = () => { return true; } handleCloseProjectButtonClick = (isSuccess: boolean, projectId: string) => { if (isSuccess) { this.allProjects.map((post: IProjectDetails) => { if (post.projectId === projectId) { post.status = 4; } }); this.showAlert(this.localize("projectCloseSuccess"), 1); this.onFilterSearchTextChange(this.filterSearchText); } else { this.showAlert(this.localize("projectCloseFailure"), 2); } } onProjectJoin = (projectId: string, isSuccess: boolean) => { if (isSuccess) { this.allProjects.map((post: IProjectDetails) => { if (post.projectId === projectId) { if (post.projectParticipantsUserIds === "") { post.projectParticipantsUserIds = post.projectParticipantsUserIds + this.loggedInUserObjectId; post.projectParticipantsUserMapping = post.projectParticipantsUserMapping + this.loggedInUserObjectId + ":" + this.loggedInUserName; } else { post.projectParticipantsUserIds = post.projectParticipantsUserIds + ";" + this.loggedInUserObjectId; post.projectParticipantsUserMapping = post.projectParticipantsUserMapping + ";" + this.loggedInUserObjectId + ":" + this.loggedInUserName; } } }); this.setState({ projectDetails: this.allProjects }) this.onFilterSearchTextChange(this.filterSearchText); this.showAlert(this.localize("projectJoinedSuccess"), 1) } else { this.showAlert(this.localize("projectJoinedFailure"), 2) } } /** * Renders the component */ public render(): JSX.Element { return ( <div> {this.getWrapperPage()} </div> ); } /** *Get wrapper for page which acts as container for all child components */ private getWrapperPage = () => { if (this.state.loader) { return ( <div className="container-div"> <div className="container-subdiv"> <div className="loader"> <Loader /> </div> </div> </div> ); } else { // Cards component array to be rendered in grid. const cards = new Array<any>(); this.state.projectDetails!.map((value: IProjectDetails, index) => { if (!value.isRemoved) { cards.push(<Col lg={3} sm={6} md={4} className="grid-column d-flex justify-content-center"> <Card loggedInUserId={this.loggedInUserObjectId} projectDetails={this.state.projectDetails} onJoinMenuItemClick={this.onProjectJoin} onCloseProjectButtonClick={this.handleCloseProjectButtonClick} onLeaveButtonClick={this.handleLeaveButtonClick} showLeaveProjects={false} showJoinProjectMenu={true} index={index} cardDetails={value} onCardUpdate={this.onCardUpdate} onDeleteButtonClick={this.handleDeleteButtonClick} /> </Col>) } }); if (this.state.initialProjects.length === 0) { return ( <div className="container-div"> <div className="container-subdiv"> <NotificationMessage onClose={this.hideAlert} showAlert={this.state.showAlert} content={this.state.alertMessage} notificationType={this.state.alertprojectStatus} /> <FilterNoPostContentPage /> </div> </div> ) } let scrollViewStyle = { height: this.state.isFilterApplied === true ? "84vh" : "92vh" }; return ( <div className="container-div"> <div className="container-subdiv-cardview"> <Container fluid className="container-fluid-overriden"> <NotificationMessage onClose={this.hideAlert} showAlert={this.state.showAlert} content={this.state.alertMessage} notificationType={this.state.alertprojectStatus} /> <TitleBar projectDetails={this.state.projectDetails} showFilter={true} teamId={this.teamId} commandBarSearchText={this.state.searchText} searchFilterProjectsUsingAPI={this.invokeApiSearch} onFilterClear={this.handleFilterClear} hideFilterbar={!this.state.isFilterApplied} onSortByChange={this.onSortByChange} onFilterSearchChange={this.onFilterSearchTextChange} onSearchInputChange={this.handleSearchInputChange} onNewProjectSubmit={this.onNewPost} onSharedByCheckboxStateChange={this.onSharedByCheckboxStateChange} onTypeCheckboxStateChange={this.onprojectStatusCheckboxStateChange} onSkilsStateChange={this.onskillsStateChange} /> <div key={this.state.infiniteScrollParentKey} className="scroll-view scroll-view-mobile" style={scrollViewStyle}> <InfiniteScroll pageStart={this.state.pageLoadStart} loadMore={this.loadMoreProjects} hasMore={this.state.hasMoreProjects && !this.filterSearchText.trim().length} initialLoad={this.state.isPageInitialLoad} useWindow={false} loader={<div className="loader"><Loader /></div>}> <Row> { cards.length ? cards : this.state.hasMoreProjects === true ? <></> : <FilterNoPostContentPage /> } </Row> </InfiniteScroll> </div> </Container> </div> </div> ); } } } export default withTranslation()(DiscoverWrapperPage)