Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to opt-in to "compatibility mode" if uncaught JS errors occur #193

Merged
merged 14 commits into from
Sep 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ frontend/static/frontend/*.js
frontend/static/frontend/*.css
frontend/static/frontend/*.map
frontend/static/frontend/report.html
frontend/safe_mode/safe-mode.min.js
staticfiles/
frontend/lib/queries/__generated__/
querybuilder.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ frontend/static/frontend/*.js
frontend/static/frontend/*.css
frontend/static/frontend/*.map
frontend/static/frontend/report.html
frontend/safe_mode/safe-mode.min.js
staticfiles/
frontend/lib/queries/__generated__/
querybuilder.js
Expand Down
19 changes: 19 additions & 0 deletions frontend/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Dict, Any
from project.util.js_snippet import JsSnippetContextProcessor
import frontend.safe_mode


class SafeModeJsSnippet(JsSnippetContextProcessor):
@property
def template(self) -> str:
return frontend.safe_mode.SAFE_MODE_JS.read_text()

var_name = 'SAFE_MODE_SNIPPET'


def safe_mode(request):
is_enabled = frontend.safe_mode.is_enabled(request)
ctx: Dict[str, Any] = {'is_safe_mode_enabled': is_enabled}
if not is_enabled:
ctx.update(SafeModeJsSnippet()(request))
return ctx
4 changes: 0 additions & 4 deletions frontend/lib/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,6 @@ export class AppWithoutRouter extends React.Component<AppPropsWithRouter, AppSta
}} />
</div>
</div>
<div className="hero-foot">
{/* This is a no-JS fallback. */}
<Navbar />
</div>
</section>
</AriaAnnouncer>
</AppContext.Provider>
Expand Down
2 changes: 0 additions & 2 deletions frontend/lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,5 @@ window.addEventListener('load', () => {
// Since JS is now loaded, let's remove that restriction.
div.removeAttribute('hidden');

document.documentElement.classList.remove('jf-no-js');

startApp(div, initialProps);
});
79 changes: 53 additions & 26 deletions frontend/lib/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ import { Link } from 'react-router-dom';
import autobind from 'autobind-decorator';
import { AriaExpandableButton } from './aria';
import { bulmaClasses } from './bulma';
import { AppContext, AppContextType } from './app-context';
import { AppContextType, withAppContext } from './app-context';
import Routes from './routes';

type Dropdown = 'developer';
type Dropdown = 'developer'|'all';

export interface NavbarProps {
}
export type NavbarProps = AppContextType;

interface NavbarState {
currentDropdown: Dropdown|null;
isHamburgerOpen: boolean;
}

export default class Navbar extends React.Component<NavbarProps, NavbarState> {
class NavbarWithoutAppContext extends React.Component<NavbarProps, NavbarState> {
navbarRef: React.RefObject<HTMLDivElement>;

constructor(props: NavbarProps) {
super(props);
this.state = { currentDropdown: null, isHamburgerOpen: false };
if (props.session.isSafeModeEnabled) {
this.state = { currentDropdown: 'all', isHamburgerOpen: true };
} else {
this.state = { currentDropdown: null, isHamburgerOpen: false };
}
this.navbarRef = React.createRef();
}

Expand Down Expand Up @@ -56,13 +59,33 @@ export default class Navbar extends React.Component<NavbarProps, NavbarState> {

componentDidMount() {
window.addEventListener('focus', this.handleFocus, true);
window.addEventListener('resize', this.handleResize, false);
}

componentWillUnmount() {
window.removeEventListener('focus', this.handleFocus, true);
window.removeEventListener('resize', this.handleResize, false);
}

isDropdownActive(dropdown: Dropdown) {
return this.state.currentDropdown === dropdown || this.state.currentDropdown === 'all';
}

@autobind
handleResize() {
this.setState({
currentDropdown: null,
isHamburgerOpen: false
});
}

renderDevMenu({ server }: AppContextType): JSX.Element|null {
@autobind
handleShowSafeModeUI() {
window.SafeMode.showUI();
}

renderDevMenu(): JSX.Element|null {
const { server, session } = this.props;
const { state } = this;

if (!server.debug) return null;
Expand All @@ -71,18 +94,21 @@ export default class Navbar extends React.Component<NavbarProps, NavbarState> {
<NavbarDropdown
name="Developer"
isHamburgerOpen={state.isHamburgerOpen}
isActive={state.currentDropdown === 'developer'}
isActive={this.isDropdownActive('developer')}
onToggle={() => this.toggleDropdown('developer')}
>
<a className="navbar-item" href={`${server.staticURL}frontend/report.html`}>Webpack analysis</a>
<a className="navbar-item" href="/graphiql">GraphiQL</a>
<a className="navbar-item" href="/loc/example.pdf">Example PDF</a>
<a className="navbar-item" href="https://github.com/JustFixNYC/tenants2">GitHub</a>
{!session.isSafeModeEnabled &&
<a className="navbar-item" href="#" onClick={this.handleShowSafeModeUI}>Show safe mode UI</a>}
</NavbarDropdown>
);
}

renderNavbarBrand({ server }: AppContextType): JSX.Element {
renderNavbarBrand(): JSX.Element {
const { server } = this.props;
const { state } = this;

return (
Expand All @@ -106,30 +132,31 @@ export default class Navbar extends React.Component<NavbarProps, NavbarState> {

render() {
const { state } = this;
const { session, server } = this.props;

return (
<AppContext.Consumer>
{appContext => (
<nav className="navbar" ref={this.navbarRef}>
<div className="container">
{this.renderNavbarBrand(appContext)}
<div className={bulmaClasses('navbar-menu', state.isHamburgerOpen && 'is-active')}>
<div className="navbar-end">
{appContext.session.isStaff && <a className="navbar-item" href={appContext.server.adminIndexURL}>Admin</a>}
{appContext.session.phoneNumber
? <Link className="navbar-item" to={Routes.logout}>Sign out</Link>
: <Link className="navbar-item" to={Routes.login}>Sign in</Link> }
{this.renderDevMenu(appContext)}
</div>
</div>
<nav className="navbar" ref={this.navbarRef}>
<div className="container">
{this.renderNavbarBrand()}
<div className={bulmaClasses('navbar-menu', state.isHamburgerOpen && 'is-active')}>
<div className="navbar-end">
{session.isStaff && <a className="navbar-item" href={server.adminIndexURL}>Admin</a>}
{session.phoneNumber
? <Link className="navbar-item" to={Routes.logout}>Sign out</Link>
: <Link className="navbar-item" to={Routes.login}>Sign in</Link> }
{this.renderDevMenu()}
</div>
</nav>
)}
</AppContext.Consumer>
</div>
</div>
</nav>
);
}
}

const Navbar = withAppContext(NavbarWithoutAppContext);

export default Navbar;

interface NavbarDropdownProps {
name: string;
children: any;
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/progressive-enhancement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class ProgressiveEnhancement extends React.Component<ProgressiveEnhanceme
componentDidCatch(error: Error) {
if (this.isEnhanced()) {
this.setState({ hasCaughtError: true });
if (window.SafeMode) {
window.SafeMode.ignoreError(error);
}
} else {
throw error;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/queries/AllSessionInfo.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ fragment AllSessionInfo on SessionInfo {
phoneNumber
csrfToken
isStaff
isSafeModeEnabled
onboardingStep1 {
name
address
Expand Down
5 changes: 5 additions & 0 deletions frontend/lib/queries/AllSessionInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export interface AllSessionInfo {
* Whether or not the currently logged-in user is a staff member.
*/
isStaff: boolean;
/**
* Whether or not the current session has safe/compatibility mode compatibility mode) enabled.
*/
isSafeModeEnabled: boolean;
onboardingStep1: AllSessionInfo_onboardingStep1 | null;
onboardingStep2: AllSessionInfo_onboardingStep2 | null;
onboardingStep3: AllSessionInfo_onboardingStep3 | null;
Expand All @@ -107,6 +111,7 @@ export const graphQL = `fragment AllSessionInfo on SessionInfo {
phoneNumber
csrfToken
isStaff
isSafeModeEnabled
onboardingStep1 {
name
address
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/queries/LoginMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export interface LoginMutation_output_session {
* Whether or not the currently logged-in user is a staff member.
*/
isStaff: boolean;
/**
* Whether or not the current session has safe/compatibility mode compatibility mode) enabled.
*/
isSafeModeEnabled: boolean;
onboardingStep1: LoginMutation_output_session_onboardingStep1 | null;
onboardingStep2: LoginMutation_output_session_onboardingStep2 | null;
onboardingStep3: LoginMutation_output_session_onboardingStep3 | null;
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/queries/LogoutMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export interface LogoutMutation_output_session {
* Whether or not the currently logged-in user is a staff member.
*/
isStaff: boolean;
/**
* Whether or not the current session has safe/compatibility mode compatibility mode) enabled.
*/
isSafeModeEnabled: boolean;
onboardingStep1: LogoutMutation_output_session_onboardingStep1 | null;
onboardingStep2: LogoutMutation_output_session_onboardingStep2 | null;
onboardingStep3: LogoutMutation_output_session_onboardingStep3 | null;
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/queries/OnboardingStep4Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export interface OnboardingStep4Mutation_output_session {
* Whether or not the currently logged-in user is a staff member.
*/
isStaff: boolean;
/**
* Whether or not the current session has safe/compatibility mode compatibility mode) enabled.
*/
isSafeModeEnabled: boolean;
onboardingStep1: OnboardingStep4Mutation_output_session_onboardingStep1 | null;
onboardingStep2: OnboardingStep4Mutation_output_session_onboardingStep2 | null;
onboardingStep3: OnboardingStep4Mutation_output_session_onboardingStep3 | null;
Expand Down
3 changes: 2 additions & 1 deletion frontend/lib/tests/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export const FakeSessionInfo: Readonly<AllSessionInfo> = {
customIssues: [],
accessDates: [],
landlordDetails: null,
letterRequest: null
letterRequest: null,
isSafeModeEnabled: false
};

export const FakeAppContext: AppContextType = {
Expand Down
1 change: 1 addition & 0 deletions frontend/safe_mode/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .safe_mode import * # noqa
23 changes: 23 additions & 0 deletions frontend/safe_mode/safe-mode-globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
interface Window {
SafeMode: {
/**
* Let safe mode know that an error has been handled, so it
* shouldn't show its UI if it sees the error.
*/
ignoreError(e: Error): void;

/**
* Report an error to safe mode. Normally safe mode
* automatically detects these via an error event
* listener, so this is only really intended to be
* used by tests.
*/
reportError(e: Error): void;

/**
* Show the safe-mode opt-in UI. This is intended primarily
* for manual testing, but client code can use it too.
*/
showUI(): void;
}
}
Loading