diff --git a/src/app/services/actions/theme-action-creator.ts b/src/app/services/actions/theme-action-creator.ts index 3193f264a..b8b476ec2 100644 --- a/src/app/services/actions/theme-action-creator.ts +++ b/src/app/services/actions/theme-action-creator.ts @@ -2,7 +2,6 @@ import { Dispatch } from 'redux'; import { saveTheme } from '../../../themes/theme-utils'; import { IAction } from '../../../types/action'; -import { IThemeChangedMessage } from '../../../types/query-runner'; import { CHANGE_THEME_SUCCESS } from '../redux-constants'; export function changeThemeSuccess(response: string): IAction { @@ -12,8 +11,7 @@ export function changeThemeSuccess(response: string): IAction { }; } -export function changeTheme(theme: IThemeChangedMessage['theme']): Function { - +export function changeTheme(theme: string): Function { saveTheme(theme); return (dispatch: Dispatch) => { diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index f59c770ec..86669cc0d 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,6 +1,8 @@ import { Announced, - IStackTokens, ITheme, styled + IStackTokens, + ITheme, + styled, } from 'office-ui-fabric-react'; import React, { Component } from 'react'; import { InjectedIntl, injectIntl } from 'react-intl'; @@ -13,7 +15,11 @@ import { componentNames, eventTypes, telemetry } from '../../telemetry'; import { loadGETheme } from '../../themes'; import { ThemeContext } from '../../themes/theme-context'; import { Mode } from '../../types/enums'; -import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner'; +import { + IInitMessage, + IQuery, + IThemeChangedMessage, +} from '../../types/query-runner'; import { IRootState } from '../../types/root'; import { ISharedQueryParams } from '../../types/share-query'; import { ISidebarProps } from '../../types/sidebar'; @@ -22,13 +28,16 @@ import { runQuery } from '../services/actions/query-action-creators'; import { setSampleQuery } from '../services/actions/query-input-action-creators'; import { clearQueryStatus } from '../services/actions/query-status-action-creator'; import { clearTermsOfUse } from '../services/actions/terms-of-use-action-creator'; -import { changeThemeSuccess } from '../services/actions/theme-action-creator'; +import { changeTheme } from '../services/actions/theme-action-creator'; import { toggleSidebar } from '../services/actions/toggle-sidebar-action-creator'; import { GRAPH_URL } from '../services/graph-constants'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; -import { appTitleDisplayOnFullScreen, appTitleDisplayOnMobileScreen } from './app-sections/AppTitle'; +import { + appTitleDisplayOnFullScreen, + appTitleDisplayOnMobileScreen, +} from './app-sections/AppTitle'; import { headerMessaging } from './app-sections/HeaderMessaging'; import { statusMessages } from './app-sections/StatusMessages'; import { termsOfUseMessage } from './app-sections/TermsOfUseMessage'; @@ -61,6 +70,7 @@ interface IAppProps { toggleSidebar: Function; signIn: Function; storeScopes: Function; + changeTheme: Function; }; } @@ -78,7 +88,7 @@ class App extends Component { this.state = { selectedVerb: 'GET', mobileScreen: false, - hideDialog: true + hideDialog: true, }; } @@ -103,7 +113,7 @@ class App extends Component { 'https://docs.microsoft.com', 'https://review.docs.microsoft.com', 'https://ppe.docs.microsoft.com', - 'https://docs.azure.cn' + 'https://docs.azure.cn', ]; // Notify host document that GE is ready to receive messages @@ -147,7 +157,8 @@ class App extends Component { } private generateQueryObjectFrom(queryParams: any) { - const { request, method, version, graphUrl, requestBody, headers } = queryParams; + const { request, method, version, graphUrl, requestBody, headers } = + queryParams; if (!request) { return null; @@ -158,7 +169,7 @@ class App extends Component { selectedVerb: method, selectedVersion: version, sampleBody: requestBody ? this.hashDecode(requestBody) : null, - sampleHeaders: (headers) ? JSON.parse(this.hashDecode(headers)) : [], + sampleHeaders: headers ? JSON.parse(this.hashDecode(headers)) : [], }; } @@ -214,7 +225,7 @@ class App extends Component { if (actions) { actions.setSampleQuery({ sampleUrl: url, - selectedVerb: verb + selectedVerb: verb, }); } @@ -232,7 +243,7 @@ class App extends Component { const requestHeaders = headers.map((header: any) => { return { name: Object.keys(header)[0], - value: Object.values(header)[0] + value: Object.values(header)[0], }; }); @@ -241,7 +252,7 @@ class App extends Component { selectedVerb: verb, sampleBody: body, selectedVersion: queryVersion, - sampleHeaders: requestHeaders + sampleHeaders: requestHeaders, }; substituteTokens(query, profile); @@ -249,12 +260,11 @@ class App extends Component { actions.setSampleQuery(query); } }, 1000); - }; public handleSelectVerb = (verb: string) => { this.setState({ - selectedVerb: verb + selectedVerb: verb, }); }; @@ -263,12 +273,10 @@ class App extends Component { const properties = { ...sidebarProperties }; properties.showSidebar = !properties.showSidebar; this.props.actions!.toggleSidebar(properties); - telemetry.trackEvent( - eventTypes.BUTTON_CLICK_EVENT, - { - ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON - }); - } + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON, + }); + }; public displayToggleButton = (mediaQueryList: any) => { const mobileScreen = mediaQueryList.matches; @@ -279,32 +287,45 @@ class App extends Component { const properties = { mobileScreen, - showSidebar + showSidebar, }; this.props.actions!.toggleSidebar(properties); - } + }; public displayAuthenticationSection = (minimised: boolean) => { - return
-
- -
-
- + return ( +
+
+ +
+
+ +
-
; - } + ); + }; public render() { const classes = classNames(this.props); - const { authenticated, graphExplorerMode, queryState, minimised, termsOfUse, sampleQuery, - actions, sidebarProperties, intl: { messages } }: any = this.props; + const { + authenticated, + graphExplorerMode, + queryState, + minimised, + termsOfUse, + sampleQuery, + actions, + sidebarProperties, + intl: { messages }, + }: any = this.props; const query = createShareLink(sampleQuery, authenticated); const sampleHeaderText = messages['Sample Queries']; // tslint:disable-next-line:no-string-literal @@ -320,7 +341,7 @@ class App extends Component { const stackTokens: IStackTokens = { childrenGap: 10, - padding: 10 + padding: 10, }; let sidebarWidth = `col-sm-12 col-lg-3 col-md-4 ${classes.sidebar}`; @@ -343,46 +364,62 @@ class App extends Component { // @ts-ignore
- +
{graphExplorerMode === Mode.Complete && (
- {mobileScreen && appTitleDisplayOnMobileScreen( - stackTokens, - classes, - this.toggleSidebar)} - - {!mobileScreen && appTitleDisplayOnFullScreen( - classes, - minimised, - this.toggleSidebar - )} + {mobileScreen && + appTitleDisplayOnMobileScreen( + stackTokens, + classes, + this.toggleSidebar + )} + + {!mobileScreen && + appTitleDisplayOnFullScreen( + classes, + minimised, + this.toggleSidebar + )}
{this.displayAuthenticationSection(minimised)}
- {showSidebar && <> - - } + {showSidebar && ( + <> + + + )}
)}
- {graphExplorerMode === Mode.TryIt && headerMessaging(classes, query)} - - {displayContent && <> -
- -
- {statusMessages(queryState, sampleQuery, actions)} - {termsOfUseMessage(termsOfUse, actions, classes, geLocale)} - { - // @ts-ignore - - } - } + {graphExplorerMode === Mode.TryIt && + headerMessaging(classes, query)} + + {displayContent && ( + <> +
+ +
+ {statusMessages(queryState, sampleQuery, actions)} + {termsOfUseMessage(termsOfUse, actions, classes, geLocale)} + { + // @ts-ignore + + } + + )}
@@ -391,8 +428,15 @@ class App extends Component { } } -const mapStateToProps = ({ sidebarProperties, theme, - queryRunnerStatus, profile, sampleQuery, termsOfUse, authToken, graphExplorerMode +const mapStateToProps = ({ + sidebarProperties, + theme, + queryRunnerStatus, + profile, + sampleQuery, + termsOfUse, + authToken, + graphExplorerMode, }: IRootState) => { const mobileScreen = !!sidebarProperties.mobileScreen; const showSidebar = !!sidebarProperties.showSidebar; @@ -407,23 +451,24 @@ const mapStateToProps = ({ sidebarProperties, theme, termsOfUse, minimised: !mobileScreen && !showSidebar, sampleQuery, - authenticated: !!authToken.token + authenticated: !!authToken.token, }; }; const mapDispatchToProps = (dispatch: Dispatch) => { return { - actions: bindActionCreators({ - clearQueryStatus, - clearTermsOfUse, - runQuery, - setSampleQuery, - toggleSidebar, - ...authActionCreators, - changeTheme: (newTheme: string) => { - return (disp: Function) => disp(changeThemeSuccess(newTheme)); - } - }, dispatch) + actions: bindActionCreators( + { + clearQueryStatus, + clearTermsOfUse, + runQuery, + setSampleQuery, + toggleSidebar, + ...authActionCreators, + changeTheme, + }, + dispatch + ), }; }; @@ -431,4 +476,4 @@ const StyledApp = styled(App, appStyles as any); const IntlApp = injectIntl(StyledApp); //@ts-ignore -export default connect(mapStateToProps, mapDispatchToProps)(IntlApp); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(IntlApp); diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index fa6a5a203..622445cf0 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -11,7 +11,7 @@ import { Panel, PanelType, PrimaryButton, - TooltipHost + TooltipHost, } from 'office-ui-fabric-react'; import React, { useEffect, useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; @@ -30,17 +30,20 @@ import { togglePermissionsPanel } from '../../services/actions/permissions-panel import { changeTheme } from '../../services/actions/theme-action-creator'; import { Permission } from '../query-runner/request/permissions'; - function Settings(props: ISettingsProps) { const dispatch = useDispatch(); - const { permissionsPanelOpen, authToken, theme: appTheme } = useSelector((state: IRootState) => state); + const { + permissionsPanelOpen, + authToken, + theme: appTheme, + } = useSelector((state: IRootState) => state); const authenticated = authToken.token; const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); const [items, setItems] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState([]); const { - intl: { messages } + intl: { messages }, }: any = props; useEffect(() => { @@ -68,7 +71,7 @@ function Settings(props: ISettingsProps) { { key: 'divider', text: '-', - itemType: DropdownMenuItemType.Divider + itemType: DropdownMenuItemType.Divider, }, { key: 'change-theme', @@ -77,7 +80,7 @@ function Settings(props: ISettingsProps) { iconName: 'Color', }, onClick: () => toggleThemeChooserDialogState(), - } + }, ]; if (authenticated) { @@ -97,7 +100,7 @@ function Settings(props: ISettingsProps) { iconName: 'SignOut', }, onClick: () => handleSignOut(), - }, + } ); } setItems(menuItems); @@ -107,11 +110,9 @@ function Settings(props: ISettingsProps) { let hidden = themeChooserDialogHidden; hidden = !hidden; hideThemeChooserDialog(hidden); - telemetry.trackEvent( - eventTypes.BUTTON_CLICK_EVENT, - { - ComponentName: componentNames.THEME_CHANGE_BUTTON - }); + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.THEME_CHANGE_BUTTON, + }); }; const handleSignOut = () => { @@ -119,15 +120,13 @@ function Settings(props: ISettingsProps) { }; const handleChangeTheme = (selectedTheme: any) => { - const newTheme: AppTheme = selectedTheme.key; + const newTheme: string = selectedTheme.key; dispatch(changeTheme(newTheme)); loadGETheme(newTheme); - telemetry.trackEvent( - eventTypes.BUTTON_CLICK_EVENT, - { - ComponentName: componentNames.SELECT_THEME_BUTTON, - SelectedTheme: selectedTheme.key.replace('-', ' ').toSentenceCase() - }); + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.SELECT_THEME_BUTTON, + SelectedTheme: selectedTheme.key.replace('-', ' ').toSentenceCase(), + }); }; const changePanelState = () => { @@ -139,21 +138,16 @@ function Settings(props: ISettingsProps) { }; const trackSelectPermissionsButtonClickEvent = () => { - telemetry.trackEvent( - eventTypes.BUTTON_CLICK_EVENT, - { - ComponentName: componentNames.VIEW_ALL_PERMISSIONS_BUTTON - }); - } + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.VIEW_ALL_PERMISSIONS_BUTTON, + }); + }; const trackReportAnIssueLinkClickEvent = () => { - telemetry.trackEvent( - eventTypes.LINK_CLICK_EVENT, - { - ComponentName: componentNames.REPORT_AN_ISSUE_LINK - }); - } - + telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { + ComponentName: componentNames.REPORT_AN_ISSUE_LINK, + }); + }; const setPermissions = (permissions: []) => { setSelectedPermissions(permissions); @@ -165,11 +159,9 @@ function Settings(props: ISettingsProps) { }; const trackOfficeDevProgramLinkClickEvent = () => { - telemetry.trackEvent( - eventTypes.LINK_CLICK_EVENT, - { - ComponentName: componentNames.OFFICE_DEV_PROGRAM_LINK - }); + telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { + ComponentName: componentNames.OFFICE_DEV_PROGRAM_LINK, + }); }; const getSelectionDetails = () => { @@ -206,7 +198,7 @@ function Settings(props: ISettingsProps) { const menuProperties = { shouldFocusOnMount: true, alignTargetEdge: true, - items + items, }; return ( @@ -214,17 +206,18 @@ function Settings(props: ISettingsProps) { + calloutProps={{ gapSpace: 0 }} + > - + menuProps={menuProperties} + />
- handleChangeTheme(selectedTheme)} + onChange={(event, selectedTheme) => + handleChangeTheme(selectedTheme) + } /> toggleThemeChooserDialogState()} /> + onClick={() => toggleThemeChooserDialogState()} + /> @@ -284,4 +279,3 @@ function Settings(props: ISettingsProps) { } export default injectIntl(Settings); - diff --git a/src/index.tsx b/src/index.tsx index cfb068726..e60fe510d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,13 +14,15 @@ import pt from 'react-intl/locale-data/pt'; import ru from 'react-intl/locale-data/ru'; import zh from 'react-intl/locale-data/zh'; import { Provider } from 'react-redux'; -import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/actions/auth-action-creators'; +import { + getAuthTokenSuccess, + getConsentedScopesSuccess, +} from './app/services/actions/auth-action-creators'; import { setDevxApiUrl } from './app/services/actions/devxApi-action-creators'; import { setGraphExplorerMode } from './app/services/actions/explorer-mode-action-creator'; import { getGraphProxyUrl } from './app/services/actions/proxy-action-creator'; import { addHistoryItem } from './app/services/actions/request-history-action-creators'; import { changeThemeSuccess } from './app/services/actions/theme-action-creator'; -import { GRAPH_API_SANDBOX_URL } from './app/services/graph-constants'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import App from './app/views/App'; import { readHistoryData } from './app/views/sidebar/history/history-utils'; @@ -36,6 +38,7 @@ import { readTheme } from './themes/theme-utils'; import { IDevxAPI } from './types/devx-api'; import { Mode } from './types/enums'; import { IHistoryItem } from './types/history'; +import { changeTheme } from './app/services/actions/theme-action-creator'; // removes the loading spinner from GE html after the app is loaded const spinner = document.getElementById('spinner'); @@ -51,8 +54,45 @@ if (apiExplorer) { initializeIcons(); -const currentTheme = readTheme(); -loadGETheme(currentTheme); +let currentTheme = readTheme() || 'light'; +function setCurrentSystemTheme(): void { + const themeFromLocalStorage = readTheme(); + + if (themeFromLocalStorage) { + currentTheme = themeFromLocalStorage; + } else { + currentTheme = getOSTheme(); + } + + applyCurrentSystemTheme(currentTheme); +} +function getOSTheme(): string { + let currentSystemTheme: string = 'light'; + const currentSystemThemeDark = window.matchMedia( + '(prefers-color-scheme: dark)' + ); + + const currentSystemThemeLight = window.matchMedia( + '(prefers-color-scheme: light)' + ); + + if (currentSystemThemeDark.matches === true) { + currentSystemTheme = 'dark'; + } else if (currentSystemThemeLight.matches === true) { + currentSystemTheme = 'light'; + } else { + currentSystemTheme = 'high-contrast'; + } + + return currentSystemTheme; +} + +function applyCurrentSystemTheme(themeToApply: string): void { + loadGETheme(themeToApply); + + // @ts-ignore + appState.dispatch(changeTheme(themeToApply)); +} const appState: any = store({ authToken: { token: false, pending: false }, @@ -69,35 +109,29 @@ const appState: any = store({ }, termsOfUse: true, theme: currentTheme, - proxyUrl: GRAPH_API_SANDBOX_URL }); +setCurrentSystemTheme(); appState.dispatch(getGraphProxyUrl()); function refreshAccessToken() { - authenticationWrapper.getToken().then((authResponse: AuthenticationResult) => { - if (authResponse && authResponse.accessToken) { - appState.dispatch(getAuthTokenSuccess(true)); - appState.dispatch(getConsentedScopesSuccess(authResponse.scopes)); - } - }).catch(() => { - // ignore the error as it means that a User login is required - }); + authenticationWrapper + .getToken() + .then((authResponse: AuthenticationResult) => { + if (authResponse && authResponse.accessToken) { + appState.dispatch(getAuthTokenSuccess(true)); + appState.dispatch(getConsentedScopesSuccess(authResponse.scopes)); + } + }) + .catch(() => { + // ignore the error as it means that a User login is required + }); } refreshAccessToken(); setInterval(refreshAccessToken, 1000 * 60 * 10); // refresh access token every 10 minutes -addLocaleData([ - ...pt, - ...de, - ...en, - ...fr, - ...jp, - ...ru, - ...zh, - ...es, -]); +addLocaleData([...pt, ...de, ...en, ...fr, ...jp, ...ru, ...zh, ...es]); const theme = new URLSearchParams(location.search).get('theme'); @@ -118,7 +152,7 @@ if (devxApiUrl && isValidHttpsUrl(devxApiUrl)) { const devxApi: IDevxAPI = { baseUrl: devxApiUrl, - parameters: '' + parameters: '', }; if (org && branchName) { @@ -140,7 +174,7 @@ readHistoryData().then((data: any) => { */ enum Workers { Json = 'json', - Editor = 'editor' + Editor = 'editor', } (window as any).MonacoEnvironment = { @@ -149,16 +183,16 @@ enum Workers { return getWorkerFor(Workers.Json); } return getWorkerFor(Workers.Editor); - } + }, }; function getWorkerFor(worker: string): string { // tslint:disable-next-line:max-line-length - const WORKER_PATH = 'https://graphstagingblobstorage.blob.core.windows.net/staging/vendor/bower_components/explorer-v2/build'; + const WORKER_PATH = + 'https://graphstagingblobstorage.blob.core.windows.net/staging/vendor/bower_components/explorer-v2/build'; return `data:text/javascript;charset=utf-8,${encodeURIComponent(` - importScripts('${WORKER_PATH}/${worker}.worker.js');` - )}`; + importScripts('${WORKER_PATH}/${worker}.worker.js');`)}`; } const telemetryProvider: ITelemetry = telemetry; @@ -167,7 +201,10 @@ telemetryProvider.initialize(); const Root = () => { return ( - + diff --git a/src/themes/theme-utils.ts b/src/themes/theme-utils.ts index b7a51b211..a66a397c3 100644 --- a/src/themes/theme-utils.ts +++ b/src/themes/theme-utils.ts @@ -1,13 +1,10 @@ -import { IThemeChangedMessage } from '../types/query-runner'; - const key = 'CURRENT_THEME'; -const defaultTheme = 'light'; -export function saveTheme(theme: IThemeChangedMessage['theme']) { +export function saveTheme(theme: string) { localStorage.setItem(key, theme); } export function readTheme() { - const theme = localStorage.getItem(key) || defaultTheme; + const theme = localStorage.getItem(key); return theme; -} \ No newline at end of file +}