mirror of
https://github.com/desktop/desktop
synced 2024-09-19 16:12:20 +00:00
Pass prop/add reused logic service
This commit is contained in:
parent
d7bb19f00c
commit
213df72ee6
|
@ -16,7 +16,7 @@ import { RelativeTime } from '../relative-time'
|
|||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { ReleaseNotesUri } from '../lib/releases'
|
||||
import { encodePathAsUrl } from '../../lib/path'
|
||||
import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer'
|
||||
import { IsTopMostService } from '../dialog/is-top-most-service'
|
||||
|
||||
const logoPath = __DARWIN__
|
||||
? 'static/logo-64x64@2x.png'
|
||||
|
@ -55,6 +55,9 @@ interface IAboutProps {
|
|||
|
||||
/** A function to call when the user wants to see Terms and Conditions. */
|
||||
readonly onShowTermsAndConditions: () => void
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface IAboutState {
|
||||
|
@ -66,11 +69,18 @@ interface IAboutState {
|
|||
* A dialog that presents information about the
|
||||
* running application such as name and version.
|
||||
*/
|
||||
export class About extends DialogStackContextConsumer<
|
||||
IAboutProps,
|
||||
IAboutState
|
||||
> {
|
||||
export class About extends React.Component<IAboutProps, IAboutState> {
|
||||
private updateStoreEventHandle: Disposable | null = null
|
||||
private isTopMostService: IsTopMostService = new IsTopMostService(
|
||||
() => {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
window.addEventListener('keyup', this.onKeyUp)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
window.removeEventListener('keyup', this.onKeyUp)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: IAboutProps) {
|
||||
super(props)
|
||||
|
@ -90,7 +100,11 @@ export class About extends DialogStackContextConsumer<
|
|||
this.onUpdateStateChanged
|
||||
)
|
||||
this.setState({ updateState: updateStore.state })
|
||||
super.componentDidMount()
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -98,17 +112,7 @@ export class About extends DialogStackContextConsumer<
|
|||
this.updateStoreEventHandle.dispose()
|
||||
this.updateStoreEventHandle = null
|
||||
}
|
||||
super.componentWillUnmount()
|
||||
}
|
||||
|
||||
protected onDialogIsTopMost() {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
window.addEventListener('keyup', this.onKeyUp)
|
||||
}
|
||||
|
||||
protected onDialogIsNotTopMost() {
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
window.removeEventListener('keyup', this.onKeyUp)
|
||||
this.isTopMostService.unmount()
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
@ -36,7 +36,7 @@ import { mkdir } from 'fs/promises'
|
|||
import { directoryExists } from '../../lib/directory-exists'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
import { join } from 'path'
|
||||
import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer'
|
||||
import { IsTopMostService } from '../dialog/is-top-most-service'
|
||||
|
||||
/** The sentinel value used to indicate no gitignore should be used. */
|
||||
const NoGitIgnoreValue = 'None'
|
||||
|
@ -72,6 +72,9 @@ interface ICreateRepositoryProps {
|
|||
|
||||
/** Prefills path input so user doesn't have to. */
|
||||
readonly initialPath?: string
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface ICreateRepositoryState {
|
||||
|
@ -112,10 +115,20 @@ interface ICreateRepositoryState {
|
|||
}
|
||||
|
||||
/** The Create New Repository component. */
|
||||
export class CreateRepository extends DialogStackContextConsumer<
|
||||
export class CreateRepository extends React.Component<
|
||||
ICreateRepositoryProps,
|
||||
ICreateRepositoryState
|
||||
> {
|
||||
private isTopMostService: IsTopMostService = new IsTopMostService(
|
||||
() => {
|
||||
this.updateReadMeExists(this.state.path, this.state.name)
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: ICreateRepositoryProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -146,7 +159,7 @@ export class CreateRepository extends DialogStackContextConsumer<
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
super.componentDidMount()
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
|
||||
const gitIgnoreNames = await getGitIgnoreNames()
|
||||
const licenses = await getLicenses()
|
||||
|
@ -159,12 +172,12 @@ export class CreateRepository extends DialogStackContextConsumer<
|
|||
this.updateReadMeExists(path, this.state.name)
|
||||
}
|
||||
|
||||
protected onDialogIsTopMost() {
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
public componentDidUpdate(): void {
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
}
|
||||
|
||||
protected onDialogIsNotTopMost() {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
public componentWillUnmount(): void {
|
||||
this.isTopMostService.unmount()
|
||||
}
|
||||
|
||||
private initializePath = async () => {
|
||||
|
|
|
@ -165,11 +165,8 @@ import { sendNonFatalException } from '../lib/helpers/non-fatal-exception'
|
|||
import { createCommitURL } from '../lib/commit-url'
|
||||
import { uuid } from '../lib/uuid'
|
||||
import { InstallingUpdate } from './installing-update/installing-update'
|
||||
import {
|
||||
DialogStackContext,
|
||||
IDialogStackContext,
|
||||
} from './dialog/dialog-stack-context-consumer'
|
||||
import { enableStackedPopups } from '../lib/feature-flag'
|
||||
import { DialogStackContext } from './dialog'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -1416,15 +1413,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return (
|
||||
<>
|
||||
{allPopups.map(popup => {
|
||||
const dialogStackContext: IDialogStackContext = {
|
||||
isTopMost: this.state.currentPopup?.id === popup.id,
|
||||
}
|
||||
const isTopMost = this.state.currentPopup?.id === popup.id
|
||||
return (
|
||||
<DialogStackContext.Provider
|
||||
key={popup.id}
|
||||
value={dialogStackContext}
|
||||
value={{
|
||||
isTopMost,
|
||||
}}
|
||||
>
|
||||
{this.popupContent(popup)}
|
||||
{this.popupContent(popup, isTopMost)}
|
||||
</DialogStackContext.Provider>
|
||||
)
|
||||
})}
|
||||
|
@ -1432,7 +1429,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
private popupContent(popup: Popup): JSX.Element | null {
|
||||
private popupContent(popup: Popup, isTopMost: boolean): JSX.Element | null {
|
||||
if (popup.id === undefined) {
|
||||
// Should not be possible... but if it does we want to know about it.
|
||||
sendNonFatalException(
|
||||
|
@ -1607,6 +1604,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onDismissed={onPopupDismissedFn}
|
||||
dispatcher={this.props.dispatcher}
|
||||
initialPath={popup.path}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.CloneRepository:
|
||||
|
@ -1622,6 +1620,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onTabSelected={this.onCloneRepositoriesTabSelected}
|
||||
apiRepositories={this.state.apiRepositories}
|
||||
onRefreshRepositories={this.onRefreshRepositories}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.CreateBranch: {
|
||||
|
@ -1682,6 +1681,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onCheckForNonStaggeredUpdates={this.onCheckForNonStaggeredUpdates}
|
||||
onShowAcknowledgements={this.showAcknowledgements}
|
||||
onShowTermsAndConditions={this.showTermsAndConditions}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.PublishRepository:
|
||||
|
|
|
@ -24,7 +24,7 @@ import { ClickSource } from '../lib/list'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { showOpenDialog, showSaveDialog } from '../main-process-proxy'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer'
|
||||
import { IsTopMostService } from '../dialog/is-top-most-service'
|
||||
|
||||
interface ICloneRepositoryProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -66,6 +66,9 @@ interface ICloneRepositoryProps {
|
|||
* available for cloning.
|
||||
*/
|
||||
readonly onRefreshRepositories: (account: Account) => void
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface ICloneRepositoryState {
|
||||
|
@ -145,10 +148,20 @@ interface IGitHubTabState extends IBaseTabState {
|
|||
}
|
||||
|
||||
/** The component for cloning a repository. */
|
||||
export class CloneRepository extends DialogStackContextConsumer<
|
||||
export class CloneRepository extends React.Component<
|
||||
ICloneRepositoryProps,
|
||||
ICloneRepositoryState
|
||||
> {
|
||||
private isTopMostService: IsTopMostService = new IsTopMostService(
|
||||
() => {
|
||||
this.validatePath()
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: ICloneRepositoryProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -194,7 +207,7 @@ export class CloneRepository extends DialogStackContextConsumer<
|
|||
this.updateUrl(this.props.initialURL || '')
|
||||
}
|
||||
|
||||
super.componentDidUpdate(prevProps)
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -203,7 +216,11 @@ export class CloneRepository extends DialogStackContextConsumer<
|
|||
this.updateUrl(initialURL)
|
||||
}
|
||||
|
||||
super.componentDidMount()
|
||||
this.isTopMostService.check(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.isTopMostService.unmount()
|
||||
}
|
||||
|
||||
private initializePath = async () => {
|
||||
|
@ -227,14 +244,6 @@ export class CloneRepository extends DialogStackContextConsumer<
|
|||
this.updateUrl(selectedTabState.url)
|
||||
}
|
||||
|
||||
protected onDialogIsTopMost() {
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
|
||||
protected onDialogIsNotTopMost() {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { error } = this.getSelectedTabState()
|
||||
return (
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
import memoizeOne from 'memoize-one'
|
||||
import * as React from 'react'
|
||||
|
||||
export interface IDialogStackContext {
|
||||
/** Whether or not this dialog is the top most one in the stack to be
|
||||
* interacted with by the user. This will also determine if event listeners
|
||||
* will be active or not. */
|
||||
isTopMost: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The DialogStackContext is used to communicate between the `Dialog` and the
|
||||
* `App` information that is mostly unique to the `Dialog` component such as
|
||||
* whether it is at the top of the popup stack. Some, but not the vast majority,
|
||||
* custom popup components in between may also utilize this to enable and
|
||||
* disable event listeners in response to changes in whether it is the top most
|
||||
* popup.
|
||||
*
|
||||
* NB *** React.Context is not the preferred method of passing data to child
|
||||
* components for this code base. We are choosing to use it here as implementing
|
||||
* prop drilling would be extremely tedious and would lead to adding `Dialog`
|
||||
* props on 60+ components that would not otherwise use them. ***
|
||||
*
|
||||
*/
|
||||
export const DialogStackContext = React.createContext<IDialogStackContext>({
|
||||
isTopMost: false,
|
||||
})
|
||||
|
||||
/**
|
||||
* A base component for any dialogs that consume the dialog stack context.
|
||||
*
|
||||
* This houses logic to respond to when the `isTopMost` changes on the
|
||||
* `DialogStackContext` by providing two abstract methods of `onDialogIsTopMost`
|
||||
* and `onDialogIsNotTopMost` and implementing a `checkWhetherDialogIsTopMost
|
||||
* method that called via the components implementations of React component
|
||||
* lifecycle methods.
|
||||
*/
|
||||
export abstract class DialogStackContextConsumer<K, T> extends React.Component<
|
||||
K,
|
||||
T
|
||||
> {
|
||||
public static contextType = DialogStackContext
|
||||
public declare context: React.ContextType<typeof DialogStackContext>
|
||||
|
||||
protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => {
|
||||
if (isTopMost) {
|
||||
this.onDialogIsTopMost()
|
||||
} else {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
})
|
||||
|
||||
/** The method called when the dialog is the top most in the stack. */
|
||||
protected abstract onDialogIsTopMost(): void
|
||||
|
||||
/** The method called when the dialog is not top most in the stack. */
|
||||
protected abstract onDialogIsNotTopMost(): void
|
||||
|
||||
public componentDidUpdate(prevProps: K): void {
|
||||
this.checkWhetherDialogIsTopMost(this.context.isTopMost)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.checkWhetherDialogIsTopMost(this.context.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
}
|
|
@ -3,8 +3,32 @@ import classNames from 'classnames'
|
|||
import { DialogHeader } from './header'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
import { getTitleBarHeight } from '../window/title-bar'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import { DialogStackContextConsumer } from './dialog-stack-context-consumer'
|
||||
import { IsTopMostService } from './is-top-most-service'
|
||||
|
||||
export interface IDialogStackContext {
|
||||
/** Whether or not this dialog is the top most one in the stack to be
|
||||
* interacted with by the user. This will also determine if event listeners
|
||||
* will be active or not. */
|
||||
isTopMost: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The DialogStackContext is used to communicate between the `Dialog` and the
|
||||
* `App` information that is mostly unique to the `Dialog` component such as
|
||||
* whether it is at the top of the popup stack. Some, but not the vast majority,
|
||||
* custom popup components in between may also utilize this to enable and
|
||||
* disable event listeners in response to changes in whether it is the top most
|
||||
* popup.
|
||||
*
|
||||
* NB *** React.Context is not the preferred method of passing data to child
|
||||
* components for this code base. We are choosing to use it here as implementing
|
||||
* prop drilling would be extremely tedious and would lead to adding `Dialog`
|
||||
* props on 60+ components that would not otherwise use them. ***
|
||||
*
|
||||
*/
|
||||
export const DialogStackContext = React.createContext<IDialogStackContext>({
|
||||
isTopMost: false,
|
||||
})
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) from when the dialog is mounted
|
||||
|
@ -139,30 +163,25 @@ interface IDialogState {
|
|||
* underlying elements. It's not possible to use the tab key to move focus
|
||||
* out of the dialog without first dismissing it.
|
||||
*/
|
||||
export class Dialog extends DialogStackContextConsumer<
|
||||
IDialogProps,
|
||||
IDialogState
|
||||
> {
|
||||
export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||
public static contextType = DialogStackContext
|
||||
public declare context: React.ContextType<typeof DialogStackContext>
|
||||
|
||||
private isTopMostService: IsTopMostService = new IsTopMostService(
|
||||
() => {
|
||||
this.onDialogIsTopMost()
|
||||
},
|
||||
() => {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
)
|
||||
|
||||
private dialogElement: HTMLDialogElement | null = null
|
||||
private dismissGraceTimeoutId?: number
|
||||
|
||||
private disableClickDismissalTimeoutId: number | null = null
|
||||
private disableClickDismissal = false
|
||||
|
||||
protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => {
|
||||
if (this.dialogElement == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isTopMost && !this.dialogElement.open) {
|
||||
this.onDialogIsTopMost()
|
||||
}
|
||||
|
||||
if (!isTopMost && this.dialogElement.open) {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Resize observer used for tracking width changes and
|
||||
* refreshing the internal codemirror instance when
|
||||
|
@ -267,12 +286,18 @@ export class Dialog extends DialogStackContextConsumer<
|
|||
this.updateTitleId()
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.isTopMostService.check(this.context.isTopMost)
|
||||
}
|
||||
|
||||
protected onDialogIsTopMost() {
|
||||
if (this.dialogElement == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dialogElement.showModal()
|
||||
if (!this.dialogElement.open) {
|
||||
this.dialogElement.showModal()
|
||||
}
|
||||
|
||||
// Provide an event that components can subscribe to in order to perform
|
||||
// tasks such as re-layout after the dialog is visible
|
||||
|
@ -295,9 +320,8 @@ export class Dialog extends DialogStackContextConsumer<
|
|||
}
|
||||
|
||||
protected onDialogIsNotTopMost() {
|
||||
if (this.dialogElement !== null) {
|
||||
if (this.dialogElement !== null && this.dialogElement.open) {
|
||||
this.dialogElement?.close()
|
||||
return
|
||||
}
|
||||
|
||||
this.clearDismissGraceTimeout()
|
||||
|
@ -463,7 +487,7 @@ export class Dialog extends DialogStackContextConsumer<
|
|||
releaseUniqueId(this.state.titleId)
|
||||
}
|
||||
|
||||
super.componentWillUnmount()
|
||||
this.isTopMostService.unmount()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IDialogProps) {
|
||||
|
@ -471,7 +495,7 @@ export class Dialog extends DialogStackContextConsumer<
|
|||
this.updateTitleId()
|
||||
}
|
||||
|
||||
super.componentDidUpdate(prevProps)
|
||||
this.isTopMostService.check(this.context.isTopMost)
|
||||
}
|
||||
|
||||
private onDialogCancel = (e: Event | React.SyntheticEvent) => {
|
||||
|
|
20
app/src/ui/dialog/is-top-most-service.tsx
Normal file
20
app/src/ui/dialog/is-top-most-service.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import memoizeOne from 'memoize-one'
|
||||
|
||||
export class IsTopMostService {
|
||||
public check = memoizeOne((isTopMost: boolean) => {
|
||||
if (isTopMost) {
|
||||
this.onDialogIsTopMost()
|
||||
} else {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
})
|
||||
|
||||
public constructor(
|
||||
private onDialogIsTopMost: () => void,
|
||||
private onDialogIsNotTopMost: () => void
|
||||
) {}
|
||||
|
||||
public unmount() {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue