+
-
+
this.props.onShowAdminSettings()}
+ onKeyDown={(event) => {
+ if (event.key === constants.enterKey)
+ this.props.onShowAdminSettings()
+ else if (!event.shiftKey)
+ this.setState({ isManageCalloutVisible: false })
+ }}
+ role="button"
+ tabIndex={0}
>
-
+
{this.props.localeStrings.adminSettingsLabel}
@@ -368,73 +605,233 @@ class Dashboard extends React.PureComponent {
-
- ) : null}
+
+
+
);
}
+ //render the sort caret on the header column for accessbility
+ customSortCaret = (order: any, column: any) => {
+ const ariaLabel = navigator.userAgent.match(/iPhone/i) ? "sortable" : "";
+ const id = column.dataField;
+ if (!order) {
+ return (
+
+
+
+
+
+
);
+ }
+ else if (order === 'asc') {
+ switch (column.dataField) {
+ case "incidentId":
+ this.setState({
+ incidentIdAriaSort: constants.sortAscAriaSort, incidentNameAriaSort: "", locationAriaSort: "",
+ severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "incidentName":
+ this.setState({
+ incidentNameAriaSort: constants.sortAscAriaSort, incidentIdAriaSort: "",
+ locationAriaSort: "", severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "location":
+ this.setState({
+ locationAriaSort: constants.sortAscAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "severity":
+ this.setState({
+ severityAriaSort: constants.sortAscAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ locationAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ })
+ break;
+ case "incidentCommanderObj":
+ this.setState({
+ incidentCommanderObjAriaSort: constants.sortAscAriaSort, incidentNameAriaSort: "",
+ incidentIdAriaSort: "", locationAriaSort: "", severityAriaSort: "", startDateAriaSort: ""
+ })
+ break;
+ default:
+ this.setState({
+ startDateAriaSort: constants.sortAscAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ locationAriaSort: "", severityAriaSort: "", incidentCommanderObjAriaSort: ""
+ });
+ }
+ return (
+
+
+
+
);
+ }
+ else if (order === 'desc') {
+ switch (column.dataField) {
+ case "incidentId":
+ this.setState({
+ incidentIdAriaSort: constants.sortDescAriaSort, incidentNameAriaSort: "", locationAriaSort: "",
+ severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "incidentName":
+ this.setState({
+ incidentNameAriaSort: constants.sortDescAriaSort, incidentIdAriaSort: "", locationAriaSort: "",
+ severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ })
+ break;
+ case "location":
+ this.setState({
+ locationAriaSort: constants.sortDescAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ severityAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "severity":
+ this.setState({
+ severityAriaSort: constants.sortDescAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ locationAriaSort: "", incidentCommanderObjAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ case "incidentCommanderObj":
+ this.setState({
+ incidentCommanderObjAriaSort: constants.sortDescAriaSort, incidentNameAriaSort: "",
+ incidentIdAriaSort: "", locationAriaSort: "", severityAriaSort: "", startDateAriaSort: ""
+ });
+ break;
+ default:
+ this.setState({
+ startDateAriaSort: constants.sortDescAriaSort, incidentNameAriaSort: "", incidentIdAriaSort: "",
+ locationAriaSort: "", severityAriaSort: "", incidentCommanderObjAriaSort: ""
+ });
+ }
+ return (
+
+
+
+
);
+ }
+ return null;
+ }
+
+ //custom header format for sortable column for accessbility
+ headerFormatter(column: any, colIndex: any, { sortElement, filterElement }: any) {
+ //adding sortable information to aria-label to fix the accessibility issue in iOS Voiceover
+ if (navigator.userAgent.match(/iPhone/i)) {
+ const id = column.dataField;
+ return (
+
+ );
+ }
+ else {
+ return (
+
+ {column.text}
+ {sortElement}
+
+ );
+ }
+ }
+
+ public onTabSelect = (event: SelectTabEvent, data: SelectTabData) => {
+ this.setState({
+ selectedTab: data.value,
+ showMapViewer: data.value === this.props.localeStrings.mapViewer ? true : false
+ });
+ };
+
public render() {
// Header object for dashboard
- const dashboardHeader = [
+ const dashboardHeader: any = [
{
dataField: 'incidentId',
text: this.props.localeStrings.incidentId,
sort: true,
- formatter: this.teamsDeepLink,
- headerTitle: true,
- title: true,
+ sortCaret: this.customSortCaret,
+ formatter: this.incidentIdFormatter,
+ headerFormatter: this.headerFormatter,
+ headerAttrs: { 'aria-sort': this.state.incidentIdAriaSort, 'role': 'columnheader', 'scope': 'col' }
}, {
dataField: 'incidentName',
text: this.props.localeStrings.incidentName,
sort: true,
- headerTitle: true,
- title: true
+ sortCaret: this.customSortCaret,
+ formatter: this.incidentNameFormatter,
+ headerFormatter: this.headerFormatter,
+ headerAttrs: { 'aria-sort': this.state.incidentNameAriaSort, 'role': 'columnheader', 'scope': 'col' }
}, {
dataField: 'severity',
text: this.props.localeStrings.fieldSeverity,
- headerTitle: true,
- title: true
+ formatter: this.severityFormatter,
+ headerAttrs: { 'aria-sort': this.state.severityAriaSort, 'role': 'columnheader', 'scope': 'col' },
+ sort: true,
+ sortValue: (cell: any) => constants.severity.indexOf(cell),
+ sortCaret: this.customSortCaret,
+ headerFormatter: this.headerFormatter
}, {
- dataField: 'incidentCommander',
+ dataField: 'incidentCommanderObj',
text: this.props.localeStrings.incidentCommander,
- headerTitle: true,
- title: true
+ formatter: this.incidentCommanderFormatter,
+ headerAttrs: { 'aria-sort': this.state.incidentCommanderObjAriaSort, 'role': 'columnheader', 'scope': 'col' },
+ sort: true,
+ sortCaret: this.customSortCaret,
+ headerFormatter: this.headerFormatter
}, {
dataField: 'status',
text: this.props.localeStrings.status,
- formatter: this.statusIcon,
- headerTitle: true,
- title: true
+ formatter: this.statusFormatter,
+ headerAttrs: { 'role': 'columnheader', 'scope': 'col', "aria-label": this.props.localeStrings.status },
+ headerFormatter: this.headerFormatter
}, {
dataField: 'location',
text: this.props.localeStrings.location,
sort: true,
- headerTitle: true,
- title: true
+ sortFunc: (a: any, b: any, order: any) => {a = JSON.parse(a).DisplayName; b = JSON.parse(b).DisplayName; return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a)},
+ sortCaret: this.customSortCaret,
+ headerFormatter: this.headerFormatter,
+ formatter: this.locationFormatter,
+ headerAttrs: { 'aria-sort': this.state.locationAriaSort, 'role': 'columnheader', 'scope': 'col' }
}, {
dataField: 'startDate',
text: this.props.localeStrings.startDate,
- headerTitle: true,
- title: true
+ formatter: this.startDateTimeFormatter,
+ headerAttrs: { 'aria-sort': this.state.startDateAriaSort, 'role': 'columnheader', 'scope': 'col' },
+ sort: true,
+ sortValue: (cell: any) => new Date(cell),
+ sortCaret: this.customSortCaret,
+ headerFormatter: this.headerFormatter
}, {
dataField: 'action',
text: this.props.localeStrings.action,
- formatter: this.onActionClick,
- headerTitle: true,
- title: true
+ formatter: this.actionFormatter,
+ headerAttrs: { 'role': 'columnheader', 'scope': 'col', "aria-label": this.props.localeStrings.action },
+ classes: `edit-icon-${this.props.currentThemeName}`,
+ headerFormatter: this.headerFormatter
}
]
+ const isDarkOrContrastTheme = this.props.currentThemeName === constants.darkMode || this.props.currentThemeName === constants.contrastMode;
return (
<>
{this.state.showLoader ?
- <>
-
- >
+
:
-
+
@@ -450,6 +847,7 @@ class Dashboard extends React.PureComponent {
onChange={(evt) => this.searchDashboard(evt)}
value={this.state.searchText}
successIndicator={false}
+ aria-describedby='noincident-all-tab noincident-active-tab noincident-planning-tab noincident-completed-tab'
/>
{this.props.isRolesEnabled ?
@@ -459,9 +857,16 @@ class Dashboard extends React.PureComponent {
-
+
-
{this.props.localeStrings.incidentDetails}
+
+ this.setState({ showMapViewer: false })}>{this.props.localeStrings.incidentDetails}
+
+ {this.props.isMapViewerEnabled ?
+ this.setState({ showMapViewer: true })}>{this.props.localeStrings.mapViewer}
+
+ : <>>}
+
@@ -484,7 +889,9 @@ class Dashboard extends React.PureComponent {
aria-label="Incidents Details"
linkFormat="tabs"
overflowBehavior='none'
- id="piv-tabs"
+ className={`pivot-tabs${isDarkOrContrastTheme ? " pivot-button-darkcontrast" : ""}`}
+ onLinkClick={(item, ev) => (this.setState({ currentTab: item?.props.headerText }))}
+ ref={this.dashboardRef}
>
{
itemKey="All"
onRenderItemLink={this._customRenderer}
>
- 10 && this.pagination}
- noDataIndication={() => ({this.props.localeStrings.noIncidentsFound}
)}
- />
+ {!this.state.showMapViewer ?
+ ({this.props.localeStrings.noIncidentsFound}
)}
+ />
+ :
+
+ }
{
itemKey="Planning"
onRenderItemLink={this._customRenderer}
>
- 10 && this.pagination}
- noDataIndication={() => ({this.props.localeStrings.noIncidentsFound}
)}
- />
+ {!this.state.showMapViewer ?
+ ({this.props.localeStrings.noIncidentsFound}
)}
+ />
+ :
+
+ }
{
itemKey="Active"
onRenderItemLink={this._customRenderer}
>
- 10 && this.pagination}
- noDataIndication={() => ({this.props.localeStrings.noIncidentsFound}
)}
- />
+ {!this.state.showMapViewer ?
+ ({this.props.localeStrings.noIncidentsFound}
)}
+ />
+ :
+
+ }
{
itemKey="Closed"
onRenderItemLink={this._customRenderer}
>
- 10 && this.pagination}
- noDataIndication={() => ({this.props.localeStrings.noIncidentsFound}
)}
- />
+ {!this.state.showMapViewer ?
+ ({this.props.localeStrings.noIncidentsFound}
)}
+ />
+ :
+
+ }
diff --git a/EOC-TeamsFx/tabs/src/components/EOCHome.tsx b/EOC-TeamsFx/tabs/src/components/EOCHome.tsx
index c8ef0a0..204711f 100644
--- a/EOC-TeamsFx/tabs/src/components/EOCHome.tsx
+++ b/EOC-TeamsFx/tabs/src/components/EOCHome.tsx
@@ -1,24 +1,28 @@
-import { initializeIcons, MessageBar, MessageBarType } from '@fluentui/react';
+import { initializeIcons, MessageBar, MessageBarType } from "@fluentui/react";
+import { FluentProvider, teamsDarkTheme, teamsHighContrastTheme, teamsLightTheme, Theme } from "@fluentui/react-components";
import { Button, Dialog, Loader } from "@fluentui/react-northstar";
-import { ApplicationInsights } from '@microsoft/applicationinsights-web';
-import { Providers, ProviderState, SimpleProvider, Graph } from '@microsoft/mgt-element';
+import loadable from "@loadable/component";
+import { ApplicationInsights } from "@microsoft/applicationinsights-web";
+import { Graph, Providers, ProviderState, SimpleProvider } from "@microsoft/mgt-element";
import { Client } from "@microsoft/microsoft-graph-client";
import * as microsoftTeams from "@microsoft/teams-js";
import { MsGraphAuthProvider, TeamsUserCredential } from "@microsoft/teamsfx";
-import React from 'react';
-import LocalizedStrings from 'react-localization';
+import "bootstrap/dist/css/bootstrap.min.css";
+import React from "react";
+import LocalizedStrings from "react-localization";
import CommonService, { IListItem } from "../common/CommonService";
-import * as constants from '../common/Constants';
-import * as graphConfig from '../common/graphConfig';
+import * as constants from "../common/Constants";
+import * as graphConfig from "../common/graphConfig";
+import siteConfig from "../config/siteConfig.json";
import { localizedStrings } from "../locale/LocaleStrings";
import "../scss/EOCHome.module.scss";
-import ActiveBridge from './ActiveBridge';
-import AdminSettings from './AdminSettings';
-import Dashboard from './Dashboard';
-import EocHeader from './EocHeader';
-import IncidentDetails from './IncidentDetails';
-import { IncidentHistory } from './IncidentHistory';
-import siteConfig from '../config/siteConfig.json';
+import EocHeader from "./EocHeader";
+const Dashboard = loadable(() => import("./Dashboard"));
+const ActiveBridge = loadable(() => import("./ActiveBridge"));
+const AdminSettings = loadable(() => import("./AdminSettings"));
+const IncidentDetails = loadable(() => import("./IncidentDetails"));
+const IncidentHistory = loadable(() => import("./IncidentHistory"));
+
initializeIcons();
//Global Variables
@@ -29,7 +33,7 @@ let siteName = process.env.REACT_APP_SHAREPOINT_SITE_NAME?.toString().replace(/\
//Get graph base URL from ARMS template(environment variable)
let graphBaseURL = process.env.REACT_APP_GRAPH_BASE_URL?.toString().replace(/\s+/g, '');
-graphBaseURL = graphBaseURL ? graphBaseURL : constants.defaultGraphBaseURL
+graphBaseURL = graphBaseURL || constants.defaultGraphBaseURL
interface IEOCHomeState {
showLoginPage: boolean;
@@ -65,6 +69,15 @@ interface IEOCHomeState {
configRoleData: any;
settingsLoader: boolean;
tenantID: any;
+ currentTeamsTheme: Theme;
+ currentThemeName: string;
+ activeDashboardIncidentId: string;
+ fromActiveDashboardTab: boolean;
+ appSettings: any;
+ isMapViewerEnabled: boolean;
+ bingMapsKeyConfigData: any;
+ appTitle: string;
+ appTitleData: any;
}
interface IEOCHomeProps {
@@ -125,8 +138,17 @@ export class EOCHome extends React.Component
{
isRolesEnabled: false,
isUserAdmin: false,
configRoleData: {},
- settingsLoader: true,
- tenantID: ""
+ settingsLoader: false,
+ tenantID: "",
+ currentTeamsTheme: teamsLightTheme,
+ currentThemeName: constants.defaultMode,
+ activeDashboardIncidentId: "",
+ fromActiveDashboardTab: false,
+ appSettings: {},
+ isMapViewerEnabled: false,
+ bingMapsKeyConfigData: {},
+ appTitle: siteConfig.appTitle,
+ appTitleData: {},
}
this.showActiveBridge = this.showActiveBridge.bind(this);
@@ -139,6 +161,19 @@ export class EOCHome extends React.Component {
await this.checkIsConsentNeeded();
try {
+ /*Identify the context of the app, whether its being opened as Personal app or from Teams tab.
+ If opened from Teams tab retrieve Incident ID from the current Teams Name*/
+ microsoftTeams.getContext(ctx => {
+ microsoftTeams.getMruTabInstances((tabInfo: any) => {
+ if (ctx.channelId && ctx.channelName && tabInfo.teamTabs[0].tabName === constants.activeDashboardTabTitle) {
+ this.setState({
+ activeDashboardIncidentId: ctx.teamSitePath?.split("_")[1] as any,
+ fromActiveDashboardTab: true
+ });
+ }
+ });
+ });
+
// get current user's language from Teams App settings
microsoftTeams.getContext(ctx => {
if (ctx && ctx.locale && ctx.locale !== "") {
@@ -155,7 +190,22 @@ export class EOCHome extends React.Component {
tenantID: ctx.tid
})
}
- })
+
+ //get current theme from the teams context
+ const theme = ctx.theme ?? constants.defaultMode;
+ this.updateTheme(theme);
+ });
+
+ //Get the app settings from the teams context. This is required to create the 'ActiveDashboard' tab
+ microsoftTeams.settings.getSettings((settings) => {
+ console.log(constants.infoLogPrefix + "settings ", settings);
+ this.setState({ appSettings: settings });
+ });
+
+ //binds the current theme to the inbuilt teams hook which is called whenever the theme changes
+ microsoftTeams.registerOnThemeChangeHandler((theme: string) => {
+ this.updateTheme(theme);
+ });
//Initialize App Insights
appInsights = new ApplicationInsights({
@@ -179,8 +229,8 @@ export class EOCHome extends React.Component {
await this.getTenantAndSiteDetails();
await this.getCurrentUserDetails();
- //method to get roles setting from config list
- await this.getRolesConfigSetting();
+ //method to get settings from config list
+ await this.getConfigSettings();
}
}
//create MS Graph client
@@ -205,6 +255,31 @@ export class EOCHome extends React.Component {
}
}
+ //method to set the current theme to state variables
+ updateTheme = (theme: string) => {
+ switch (theme.toLocaleLowerCase()) {
+ case constants.defaultMode:
+ this.setState({
+ currentTeamsTheme: teamsLightTheme,
+ currentThemeName: constants.defaultMode
+ });
+ break;
+ case constants.darkMode:
+ this.setState({
+ currentTeamsTheme: teamsDarkTheme,
+ currentThemeName: constants.darkMode
+ });
+ break;
+ case constants.contrastMode:
+ this.setState({
+ currentTeamsTheme: teamsHighContrastTheme,
+ currentThemeName: constants.contrastMode
+ });
+ break;
+ }
+ };
+
+
// Initialize the toolkit and get access token
async initGraphToolkit(credential: any, scopeVar: any) {
@@ -328,17 +403,34 @@ export class EOCHome extends React.Component {
}
//Get data from TEOC-Config sharepoint list
- private getRolesConfigSetting = async () => {
+ private getConfigSettings = async () => {
try {
+ this.setState({
+ settingsLoader: true
+ });
//graph endpoint to get data from TEOC-Config list
let graphEndpoint = `${graphConfig.spSiteGraphEndpoint}${this.state.siteId}/lists/${siteConfig.configurationList}/items?$expand=fields&$Top=5000`;
- const configData = await this.dataService.getConfigData(graphEndpoint, this.state.graph, 'EnableRoles');
-
+ const configDataRecords = [constants.enableRoles, constants.bingMapsKey, constants.appTitleKey];
+ const configData = await this.dataService.getConfigData(graphEndpoint, this.state.graph, configDataRecords);
await this.checkUserRoleIsAdmin();
+ const appTitleItem = configData.filter((item: any) => item.title === constants.appTitleKey);
+ const bingMapItem = configData.filter((item: any) => item.title === constants.bingMapsKey);
+ if (appTitleItem.length > 0) {
+ this.setState({
+ appTitle: appTitleItem[0].value,
+ appTitleData: appTitleItem[0]
+ });
+ }
+ if (bingMapItem.length > 0) {
+ this.setState({
+ isMapViewerEnabled: bingMapItem[0].value?.trim() !== "" && bingMapItem[0].value?.trim() !== undefined,
+ bingMapsKeyConfigData: bingMapItem[0]
+ });
+ }
this.setState({
- isRolesEnabled: configData.value === "True",
- configRoleData: configData,
+ isRolesEnabled: configData[0].value === "True",
+ configRoleData: configData[0],
settingsLoader: false
});
}
@@ -347,6 +439,9 @@ export class EOCHome extends React.Component {
constants.errorLogPrefix + `${constants.componentNames.EOCHomeComponent}_getConfigSetting \n`,
JSON.stringify(error)
);
+ this.setState({
+ settingsLoader: false
+ });
// Log Exception
this.dataService.trackException(appInsights, error,
constants.componentNames.EOCHomeComponent,
@@ -624,7 +719,7 @@ export class EOCHome extends React.Component {
localeStrings.setLanguage(this.state.locale);
}
return (
- <>
+
{this.state.locale === "" ?
<>
@@ -633,7 +728,11 @@ export class EOCHome extends React.Component {
<>
{ }}
localeStrings={localeStrings}
- currentUserName={this.state.currentUserName} />
+ currentUserName={this.state.currentUserName}
+ currentThemeName={this.state.currentThemeName}
+ appTitle={this.state.appTitle}
+ />
+
{this.state.showLoginPage &&
@@ -649,6 +748,8 @@ export class EOCHome extends React.Component
{
dismissButtonAriaLabel="Close"
onDismiss={() => this.setState({ showSuccessMessageBar: false, successMessage: "" })}
className="message-bar"
+ role="alert"
+ aria-live="polite"
>
{this.state.successMessage}
@@ -662,6 +763,8 @@ export class EOCHome extends React.Component {
dismissButtonAriaLabel="Close"
onDismiss={() => this.setState({ showErrorMessageBar: false, errorMessage: "" })}
className="message-bar"
+ role="alert"
+ aria-live="polite"
>
{this.state.errorMessage}
@@ -674,6 +777,8 @@ export class EOCHome extends React.Component {
: this.state.showAdminSettings ?
{
setState={this.setState}
tenantName={this.state.tenantName}
siteName={siteName}
+ currentThemeName={this.state.currentThemeName}
+ isMapViewerEnabled={this.state.isMapViewerEnabled}
+ bingMapsKeyConfigData={this.state.bingMapsKeyConfigData}
/>
: this.state.showIncidentHistory ?
{
showMessageBar={this.showMessageBar}
hideMessageBar={this.hideMessageBar}
incidentId={this.state.incidentId}
+ currentThemeName={this.state.currentThemeName}
/>
: this.state.showActiveBridge ?
<>
@@ -720,7 +829,9 @@ export class EOCHome extends React.Component {
onEditButtonClick={this.showEditForm}
graphContextURL={this.state.graphContextURL}
tenantID={this.state.tenantID}
- /> :
+ fromActiveDashboardTab={this.state.fromActiveDashboardTab}
+ />
+ :