mirror of
https://github.com/desktop/desktop
synced 2024-08-24 18:57:33 +00:00
Merge branch 'development' into refactor-remote-app-get-path-to-main-process
This commit is contained in:
commit
eca362baeb
|
@ -2,7 +2,6 @@ root: true
|
|||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
- babel
|
||||
- react
|
||||
- json
|
||||
- jsdoc
|
||||
|
@ -100,9 +99,7 @@ rules:
|
|||
'@typescript-eslint/no-explicit-any': off
|
||||
'@typescript-eslint/no-inferrable-types': off
|
||||
'@typescript-eslint/no-empty-function': off
|
||||
|
||||
# Babel
|
||||
babel/no-invalid-this: error
|
||||
'@typescript-eslint/no-redeclare': error
|
||||
|
||||
# React
|
||||
react/jsx-boolean-value:
|
||||
|
@ -114,6 +111,10 @@ rules:
|
|||
react/jsx-uses-vars: error
|
||||
react/jsx-uses-react: error
|
||||
react/no-unused-state: error
|
||||
react/no-unused-prop-types: error
|
||||
react/prop-types:
|
||||
- error
|
||||
- ignore: ['children']
|
||||
|
||||
# JSDoc
|
||||
jsdoc/check-alignment: error
|
||||
|
@ -136,9 +137,9 @@ rules:
|
|||
###########
|
||||
curly: error
|
||||
no-new-wrappers: error
|
||||
no-redeclare:
|
||||
- error
|
||||
- builtinGlobals: true
|
||||
# We'll use no-redeclare from @typescript/eslint-plugin instead as that
|
||||
# supports overloads
|
||||
no-redeclare: off
|
||||
no-eval: error
|
||||
no-sync: error
|
||||
no-var: error
|
||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [14.15.4]
|
||||
node: [14.17.0]
|
||||
os: [macos-10.15, windows-2019]
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
|
|
|
@ -1 +1 @@
|
|||
14.15.1
|
||||
14.17.0
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
python 3.9.5
|
||||
nodejs 14.15.1
|
||||
nodejs 14.17.0
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "2.9.6",
|
||||
"version": "2.9.7-beta1",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -26,6 +26,7 @@
|
|||
"codemirror-mode-elixir": "^1.1.2",
|
||||
"compare-versions": "^3.6.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
"desktop-notifications": "^0.1.4",
|
||||
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.8",
|
||||
"detect-arm64-translation": "https://github.com/desktop/node-detect-arm64-translation#v1.0.4",
|
||||
"dexie": "^2.0.0",
|
||||
|
@ -41,7 +42,7 @@
|
|||
"fs-admin": "^0.19.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"fuzzaldrin-plus": "^0.6.0",
|
||||
"keytar": "^7.7.0",
|
||||
"keytar": "^7.8.0",
|
||||
"marked": "^4.0.10",
|
||||
"mem": "^4.3.0",
|
||||
"memoize-one": "^4.0.3",
|
||||
|
|
|
@ -165,5 +165,5 @@ export function enablePullRequestQuickView(): boolean {
|
|||
|
||||
/** Should we enable high-signal notifications? */
|
||||
export function enableHighSignalNotifications(): boolean {
|
||||
return enableDevelopmentFeatures()
|
||||
return __DARWIN__ ? enableBetaFeatures() : enableDevelopmentFeatures()
|
||||
}
|
||||
|
|
55
app/src/lib/find-toast-activator-clsid.ts
Normal file
55
app/src/lib/find-toast-activator-clsid.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { shell } from 'electron'
|
||||
|
||||
/**
|
||||
* Checks all Windows shortcuts created by Squirrel looking for the toast
|
||||
* activator CLSID needed to handle Windows notifications from the Action Center.
|
||||
*/
|
||||
export function findToastActivatorClsid() {
|
||||
const shortcutPaths = [
|
||||
path.join(
|
||||
os.homedir(),
|
||||
'AppData',
|
||||
'Roaming',
|
||||
'Microsoft',
|
||||
'Windows',
|
||||
'Start Menu',
|
||||
'Programs',
|
||||
'GitHub, Inc',
|
||||
'GitHub Desktop.lnk'
|
||||
),
|
||||
path.join(os.homedir(), 'Desktop', 'GitHub Desktop.lnk'),
|
||||
]
|
||||
|
||||
for (const shortcutPath of shortcutPaths) {
|
||||
const toastActivatorClsid = findToastActivatorClsidInShorcut(shortcutPath)
|
||||
|
||||
if (toastActivatorClsid !== undefined) {
|
||||
return toastActivatorClsid
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function findToastActivatorClsidInShorcut(shortcutPath: string) {
|
||||
try {
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath)
|
||||
|
||||
if (
|
||||
shortcutDetails.toastActivatorClsid === undefined ||
|
||||
shortcutDetails.toastActivatorClsid === ''
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return shortcutDetails.toastActivatorClsid
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error looking for toast activator CLSID in shortcut ${shortcutPath}`,
|
||||
error
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ export interface IMatch<T> {
|
|||
|
||||
export type KeyFunction<T> = (item: T) => ReadonlyArray<string>
|
||||
|
||||
export function match<T, _K extends keyof T>(
|
||||
export function match<T>(
|
||||
query: string,
|
||||
items: ReadonlyArray<T>,
|
||||
getKey: KeyFunction<T>
|
||||
|
|
84
app/src/lib/globals.d.ts
vendored
84
app/src/lib/globals.d.ts
vendored
|
@ -55,57 +55,6 @@ declare const __UPDATES_URL__: string
|
|||
*/
|
||||
declare const __PROCESS_KIND__: 'main' | 'ui' | 'crash' | 'highlighter'
|
||||
|
||||
/**
|
||||
* The IdleDeadline interface is used as the data type of the input parameter to
|
||||
* idle callbacks established by calling Window.requestIdleCallback(). It offers
|
||||
* a method, timeRemaining(), which lets you determine how much longer the user
|
||||
* agent estimates it will remain idle and a property, didTimeout, which lets
|
||||
* you determine if your callback is executing because its timeout duration
|
||||
* expired.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
|
||||
*/
|
||||
interface IdleDeadline {
|
||||
readonly didTimeout: boolean
|
||||
readonly timeRemaining: () => DOMHighResTimeStamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains optional configuration parameters for the requestIdleCallback
|
||||
* function.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||
*/
|
||||
interface IdleCallbackOptions {
|
||||
/**
|
||||
* If timeout is specified and has a positive value, and the callback has not
|
||||
* already been called by the time timeout milliseconds have passed, the
|
||||
* timeout will be called during the next idle period, even if doing so risks
|
||||
* causing a negative performance impact..
|
||||
*/
|
||||
readonly timeout: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The window.requestIdleCallback() method queues a function to be called during
|
||||
* a browser's idle periods. This enables developers to perform background and
|
||||
* low priority work on the main event loop, without impacting latency-critical
|
||||
* events such as animation and input response. Functions are generally called
|
||||
* in first-in-first-out order; however, callbacks which have a timeout
|
||||
* specified may be called out-of-order if necessary in order to run them before
|
||||
* the timeout elapses.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||
*
|
||||
* @param options Contains optional configuration parameters. Currently only one
|
||||
* property is defined:
|
||||
* timeout:
|
||||
*/
|
||||
declare function requestIdleCallback(
|
||||
fn: (deadline: IdleDeadline) => void,
|
||||
options?: IdleCallbackOptions
|
||||
): number
|
||||
|
||||
interface IDesktopLogger {
|
||||
/**
|
||||
* Writes a log message at the 'error' level.
|
||||
|
@ -181,9 +130,6 @@ declare const log: IDesktopLogger
|
|||
|
||||
declare namespace NodeJS {
|
||||
interface Process extends EventEmitter {
|
||||
once(event: 'exit', listener: Function): this
|
||||
once(event: 'uncaughtException', listener: (error: Error) => void): this
|
||||
on(event: 'uncaughtException', listener: (error: Error) => void): this
|
||||
on(
|
||||
event: 'send-non-fatal-exception',
|
||||
listener: (error: Error, context?: { [key: string]: string }) => void
|
||||
|
@ -197,21 +143,7 @@ declare namespace NodeJS {
|
|||
}
|
||||
}
|
||||
|
||||
interface XMLHttpRequest extends XMLHttpRequestEventTarget {
|
||||
/**
|
||||
* Initiates the request. The optional argument provides the request body. The argument is ignored if request method is GET or HEAD.
|
||||
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
|
||||
*/
|
||||
send(body?: Document | BodyInit | null): void
|
||||
}
|
||||
|
||||
declare namespace Electron {
|
||||
interface RequestOptions {
|
||||
readonly method: string
|
||||
readonly url: string
|
||||
readonly headers: any
|
||||
}
|
||||
|
||||
type AppleActionOnDoubleClickPref = 'Maximize' | 'Minimize' | 'None'
|
||||
|
||||
interface SystemPreferences {
|
||||
|
@ -222,19 +154,6 @@ declare namespace Electron {
|
|||
}
|
||||
}
|
||||
|
||||
// https://wicg.github.io/ResizeObserver/#resizeobserverentry
|
||||
interface IResizeObserverEntry {
|
||||
readonly target: HTMLElement
|
||||
readonly contentRect: ClientRect
|
||||
}
|
||||
|
||||
declare class ResizeObserver {
|
||||
public constructor(cb: (entries: ReadonlyArray<IResizeObserverEntry>) => void)
|
||||
|
||||
public disconnect(): void
|
||||
public observe(e: HTMLElement): void
|
||||
}
|
||||
|
||||
declare module 'file-metadata' {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
function fileMetadata(path: string): Promise<plist.PlistObject>
|
||||
|
@ -246,6 +165,9 @@ interface Window {
|
|||
Element: typeof Element
|
||||
}
|
||||
|
||||
interface HTMLDialogElement {
|
||||
showModal: () => void
|
||||
}
|
||||
/**
|
||||
* Obtain the number of elements of a tuple type
|
||||
*
|
||||
|
|
|
@ -85,10 +85,12 @@ export type RequestChannels = {
|
|||
export type RequestResponseChannels = {
|
||||
'get-path': (path: PathType) => Promise<string>
|
||||
'get-app-architecture': () => Promise<Architecture>
|
||||
'get-app-path': () => Promise<string>
|
||||
'is-running-under-rosetta-translation': () => Promise<boolean>
|
||||
'move-to-trash': (path: string) => Promise<void>
|
||||
'show-contextual-menu': (
|
||||
items: ReadonlyArray<ISerializableMenuItem>
|
||||
items: ReadonlyArray<ISerializableMenuItem>,
|
||||
addSpellCheckMenu: boolean
|
||||
) => Promise<ReadonlyArray<number> | null>
|
||||
'is-window-focused': () => Promise<boolean>
|
||||
'open-external': (path: string) => Promise<boolean>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { invokeContextualMenu } from '../ui/main-process-proxy'
|
||||
|
||||
export interface IMenuItem {
|
||||
/** The user-facing label. */
|
||||
readonly label?: string
|
||||
|
@ -72,3 +74,58 @@ export function getPlatformSpecificNameOrSymbolForModifier(
|
|||
// Not a known modifier, likely a normal key
|
||||
return modifier
|
||||
}
|
||||
|
||||
/** Show the given menu items in a contextual menu. */
|
||||
export async function showContextualMenu(
|
||||
items: ReadonlyArray<IMenuItem>,
|
||||
addSpellCheckMenu = false
|
||||
) {
|
||||
const indices = await invokeContextualMenu(
|
||||
serializeMenuItems(items),
|
||||
addSpellCheckMenu
|
||||
)
|
||||
|
||||
if (indices !== null) {
|
||||
const menuItem = findSubmenuItem(items, indices)
|
||||
|
||||
if (menuItem !== undefined && menuItem.action !== undefined) {
|
||||
menuItem.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the menu items properties that can't be serializable in
|
||||
* order to pass them via IPC.
|
||||
*/
|
||||
function serializeMenuItems(
|
||||
items: ReadonlyArray<IMenuItem>
|
||||
): ReadonlyArray<ISerializableMenuItem> {
|
||||
return items.map(item => ({
|
||||
...item,
|
||||
action: undefined,
|
||||
submenu: item.submenu ? serializeMenuItems(item.submenu) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the submenus of the context menu until we find the appropriate index.
|
||||
*/
|
||||
function findSubmenuItem(
|
||||
currentContextualMenuItems: ReadonlyArray<IMenuItem>,
|
||||
indices: ReadonlyArray<number>
|
||||
): IMenuItem | undefined {
|
||||
let foundMenuItem: IMenuItem | undefined = {
|
||||
submenu: currentContextualMenuItems,
|
||||
}
|
||||
|
||||
for (const index of indices) {
|
||||
if (foundMenuItem === undefined || foundMenuItem.submenu === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
foundMenuItem = foundMenuItem.submenu[index]
|
||||
}
|
||||
|
||||
return foundMenuItem
|
||||
}
|
||||
|
|
|
@ -23,9 +23,6 @@ let oauthState: IOAuthState | null = null
|
|||
* flow.
|
||||
*/
|
||||
export function askUserToOAuth(endpoint: string) {
|
||||
// Disable the lint warning since we're storing the `resolve` and `reject`
|
||||
// functions.
|
||||
// tslint:disable-next-line:promise-must-complete
|
||||
return new Promise<Account>((resolve, reject) => {
|
||||
oauthState = { state: uuid(), endpoint, resolve, reject }
|
||||
|
||||
|
|
|
@ -90,8 +90,8 @@ export async function getChangeLog(
|
|||
'https://central.github.com/deployments/desktop/desktop/changelog.json'
|
||||
)
|
||||
|
||||
if (__RELEASE_CHANNEL__ === 'beta') {
|
||||
changelogURL.searchParams.set('env', 'beta')
|
||||
if (__RELEASE_CHANNEL__ === 'beta' || __RELEASE_CHANNEL__ === 'test') {
|
||||
changelogURL.searchParams.set('env', __RELEASE_CHANNEL__)
|
||||
}
|
||||
|
||||
if (limit !== undefined) {
|
||||
|
|
|
@ -130,7 +130,7 @@ export class ApiRepositoriesStore extends BaseStore {
|
|||
this.emitUpdate()
|
||||
}
|
||||
|
||||
private updateAccount<T, K extends keyof IAccountRepositories>(
|
||||
private updateAccount<K extends keyof IAccountRepositories>(
|
||||
account: Account,
|
||||
repositories: Pick<IAccountRepositories, K>
|
||||
) {
|
||||
|
|
|
@ -739,8 +739,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
/** Load the emoji from disk. */
|
||||
public loadEmoji() {
|
||||
const rootDir = getAppPath()
|
||||
public async loadEmoji() {
|
||||
const rootDir = await getAppPath()
|
||||
readEmoji(rootDir)
|
||||
.then(emoji => {
|
||||
this.emoji = emoji
|
||||
|
@ -5205,7 +5205,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
* resolve when `_completeOpenInDesktop` is called.
|
||||
*/
|
||||
public _startOpenInDesktop(fn: () => void): Promise<Repository | null> {
|
||||
// tslint:disable-next-line:promise-must-complete
|
||||
const p = new Promise<Repository | null>(
|
||||
resolve => (this.resolveOpenInDesktop = resolve)
|
||||
)
|
||||
|
|
|
@ -140,8 +140,6 @@ export class RepositoryIndicatorUpdater {
|
|||
|
||||
public pause() {
|
||||
if (this.paused === false) {
|
||||
// Disable the lint warning since we're storing the `resolve`
|
||||
// tslint:disable-next-line:promise-must-complete
|
||||
this.pausePromise = new Promise<void>(resolve => {
|
||||
this.resolvePausePromise = resolve
|
||||
})
|
||||
|
|
|
@ -1,4 +1,32 @@
|
|||
import { focusWindow } from '../../../ui/main-process-proxy'
|
||||
import {
|
||||
DesktopNotification,
|
||||
initializeNotifications,
|
||||
} from 'desktop-notifications'
|
||||
import { findToastActivatorClsid } from '../../find-toast-activator-clsid'
|
||||
|
||||
let windowsToastActivatorClsid: string | undefined = undefined
|
||||
|
||||
function initializeWindowsNotifications() {
|
||||
if (windowsToastActivatorClsid !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
windowsToastActivatorClsid = findToastActivatorClsid()
|
||||
|
||||
if (windowsToastActivatorClsid === undefined) {
|
||||
log.error(
|
||||
'Toast activator CLSID not found in any of the shortucts. Falling back to known CLSIDs.'
|
||||
)
|
||||
|
||||
// This is generated by Squirrel.Windows here:
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/7396fa50ccebf97e28c79ef519c07cb9eee121be/src/Squirrel/UpdateManager.ApplyReleases.cs#L258
|
||||
windowsToastActivatorClsid = '{27D44D0C-A542-5B90-BCDB-AC3126048BA2}'
|
||||
}
|
||||
|
||||
log.info(`Using toast activator CLSID ${windowsToastActivatorClsid}`)
|
||||
initializeNotifications(windowsToastActivatorClsid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notification with a title, a body, and a function to handle when the
|
||||
|
@ -9,6 +37,18 @@ export function showNotification(
|
|||
body: string,
|
||||
onClick: () => void
|
||||
) {
|
||||
if (__WIN32__) {
|
||||
initializeWindowsNotifications()
|
||||
|
||||
const notification = new DesktopNotification(title, body)
|
||||
notification.onclick = () => {
|
||||
focusWindow()
|
||||
onClick()
|
||||
}
|
||||
notification.show()
|
||||
return
|
||||
}
|
||||
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
})
|
||||
|
|
|
@ -35,14 +35,6 @@ export async function reportError(
|
|||
}
|
||||
}
|
||||
|
||||
const requestOptions: Electron.RequestOptions = {
|
||||
method: 'POST',
|
||||
url: nonFatal ? NonFatalErrorEndpoint : ErrorEndpoint,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
|
||||
const body = [...data.entries()]
|
||||
.map(
|
||||
([key, value]) =>
|
||||
|
@ -52,7 +44,10 @@ export async function reportError(
|
|||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = net.request(requestOptions)
|
||||
const url = nonFatal ? NonFatalErrorEndpoint : ErrorEndpoint
|
||||
const request = net.request({ method: 'POST', url })
|
||||
|
||||
request.setHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
request.on('response', response => {
|
||||
if (response.statusCode === 200) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import { installSameOriginFilter } from './same-origin-filter'
|
|||
import * as ipcMain from './ipc-main'
|
||||
import { getArchitecture } from '../lib/get-architecture'
|
||||
import * as remoteMain from '@electron/remote/main'
|
||||
import { buildSpellCheckMenu } from './menu/build-spell-check-menu'
|
||||
remoteMain.initialize()
|
||||
|
||||
app.setAppLogsPath()
|
||||
|
@ -450,14 +451,24 @@ app.on('ready', () => {
|
|||
* Handle the action to show a contextual menu.
|
||||
*
|
||||
* It responds an array of indices that maps to the path to reach
|
||||
* the menu (or submenu) item that was clicked or null if the menu
|
||||
* was closed without clicking on any item.
|
||||
* the menu (or submenu) item that was clicked or null if the menu was closed
|
||||
* without clicking on any item or the item click was handled by the main
|
||||
* process as opposed to the renderer.
|
||||
*/
|
||||
ipcMain.handle('show-contextual-menu', (event, items) => {
|
||||
return new Promise(resolve => {
|
||||
const menu = buildContextMenu(items, indices => resolve(indices))
|
||||
ipcMain.handle('show-contextual-menu', (event, items, addSpellCheckMenu) => {
|
||||
return new Promise(async resolve => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender) || undefined
|
||||
|
||||
const spellCheckMenuItems = addSpellCheckMenu
|
||||
? await buildSpellCheckMenu(window)
|
||||
: undefined
|
||||
|
||||
const menu = buildContextMenu(
|
||||
items,
|
||||
indices => resolve(indices),
|
||||
spellCheckMenuItems
|
||||
)
|
||||
|
||||
menu.popup({ window, callback: () => resolve(null) })
|
||||
})
|
||||
})
|
||||
|
@ -546,6 +557,11 @@ app.on('ready', () => {
|
|||
*/
|
||||
ipcMain.handle('get-app-architecture', async () => getArchitecture(app))
|
||||
|
||||
/**
|
||||
* An event sent by the renderer asking for the app's path
|
||||
*/
|
||||
ipcMain.handle('get-app-path', async () => app.getAppPath())
|
||||
|
||||
/**
|
||||
* An event sent by the renderer asking for whether the app is running under
|
||||
* rosetta translation
|
||||
|
|
|
@ -48,9 +48,20 @@ function getEditMenuItems(): ReadonlyArray<MenuItem> {
|
|||
*/
|
||||
export function buildContextMenu(
|
||||
template: ReadonlyArray<ISerializableMenuItem>,
|
||||
onClick: (indices: ReadonlyArray<number>) => void
|
||||
onClick: (indices: ReadonlyArray<number>) => void,
|
||||
spellCheckMenuItems?: ReadonlyArray<MenuItem>
|
||||
): Menu {
|
||||
return buildRecursiveContextMenu(template, onClick)
|
||||
const menu = buildRecursiveContextMenu(template, onClick)
|
||||
|
||||
if (spellCheckMenuItems === undefined) {
|
||||
return menu
|
||||
}
|
||||
|
||||
for (const spellCheckMenuItem of spellCheckMenuItems) {
|
||||
menu.append(spellCheckMenuItem)
|
||||
}
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
function buildRecursiveContextMenu(
|
||||
|
|
111
app/src/main-process/menu/build-spell-check-menu.ts
Normal file
111
app/src/main-process/menu/build-spell-check-menu.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { app, BrowserWindow, MenuItem } from 'electron'
|
||||
|
||||
export async function buildSpellCheckMenu(
|
||||
window: BrowserWindow | undefined
|
||||
): Promise<ReadonlyArray<MenuItem> | undefined> {
|
||||
if (window === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
When a user right clicks on a misspelled word in an input, we get event from
|
||||
electron. That event comes after the context menu event that we get from the
|
||||
dom.
|
||||
*/
|
||||
return new Promise(resolve => {
|
||||
window.webContents.once('context-menu', (event, params) =>
|
||||
resolve(getSpellCheckMenuItems(event, params, window.webContents))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function getSpellCheckMenuItems(
|
||||
event: Electron.Event,
|
||||
params: Electron.ContextMenuParams,
|
||||
webContents: Electron.WebContents
|
||||
): ReadonlyArray<MenuItem> | undefined {
|
||||
const { misspelledWord, dictionarySuggestions } = params
|
||||
if (!misspelledWord && dictionarySuggestions.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const items = new Array<MenuItem>()
|
||||
|
||||
items.push(
|
||||
new MenuItem({
|
||||
type: 'separator',
|
||||
})
|
||||
)
|
||||
|
||||
for (const suggestion of dictionarySuggestions) {
|
||||
items.push(
|
||||
new MenuItem({
|
||||
label: suggestion,
|
||||
click: () => webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (misspelledWord) {
|
||||
items.push(
|
||||
new MenuItem({
|
||||
label: __DARWIN__ ? 'Add to Dictionary' : 'Add to dictionary',
|
||||
click: () =>
|
||||
webContents.session.addWordToSpellCheckerDictionary(misspelledWord),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!__DARWIN__) {
|
||||
// NOTE: "On macOS as we use the native APIs there is no way to set the
|
||||
// language that the spellchecker uses" -- electron docs Therefore, we are
|
||||
// only allowing setting to English for non-mac machines.
|
||||
const spellCheckLanguageItem = getSpellCheckLanguageMenuItem(
|
||||
webContents.session
|
||||
)
|
||||
if (spellCheckLanguageItem !== null) {
|
||||
items.push(spellCheckLanguageItem)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a menu item to give user the option to use English or their
|
||||
* system language.
|
||||
*
|
||||
* If system language is english, it returns null. If spellchecker is not set to
|
||||
* english, it returns item that can set it to English. If spellchecker is set
|
||||
* to english, it returns the item that can set it to their system language.
|
||||
*/
|
||||
function getSpellCheckLanguageMenuItem(
|
||||
session: Electron.session
|
||||
): MenuItem | null {
|
||||
const userLanguageCode = app.getLocale()
|
||||
const englishLanguageCode = 'en-US'
|
||||
const spellcheckLanguageCodes = session.getSpellCheckerLanguages()
|
||||
|
||||
if (
|
||||
userLanguageCode === englishLanguageCode &&
|
||||
spellcheckLanguageCodes.includes(englishLanguageCode)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const languageCode =
|
||||
spellcheckLanguageCodes.includes(englishLanguageCode) &&
|
||||
!spellcheckLanguageCodes.includes(userLanguageCode)
|
||||
? userLanguageCode
|
||||
: englishLanguageCode
|
||||
|
||||
const label =
|
||||
languageCode === englishLanguageCode
|
||||
? 'Set spellcheck to English'
|
||||
: 'Set spellcheck to system language'
|
||||
|
||||
return new MenuItem({
|
||||
label,
|
||||
click: () => session.setSpellCheckerLanguages([languageCode]),
|
||||
})
|
||||
}
|
|
@ -95,10 +95,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
|||
}
|
||||
|
||||
private renderUpdateButton() {
|
||||
if (
|
||||
__RELEASE_CHANNEL__ === 'development' ||
|
||||
__RELEASE_CHANNEL__ === 'test'
|
||||
) {
|
||||
if (__RELEASE_CHANNEL__ === 'development') {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -180,14 +177,11 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
|||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
__RELEASE_CHANNEL__ === 'development' ||
|
||||
__RELEASE_CHANNEL__ === 'test'
|
||||
) {
|
||||
if (__RELEASE_CHANNEL__ === 'development') {
|
||||
return (
|
||||
<p>
|
||||
The application is currently running in development or test mode and
|
||||
will not receive any updates.
|
||||
The application is currently running in development and will not
|
||||
receive any updates.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -216,10 +210,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
|||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
__RELEASE_CHANNEL__ === 'development' ||
|
||||
__RELEASE_CHANNEL__ === 'test'
|
||||
) {
|
||||
if (__RELEASE_CHANNEL__ === 'development') {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -42,8 +42,8 @@ export class Acknowledgements extends React.Component<
|
|||
this.state = { licenses: null }
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const path = Path.join(getAppPath(), 'static', 'licenses.json')
|
||||
public async componentDidMount() {
|
||||
const path = Path.join(await getAppPath(), 'static', 'licenses.json')
|
||||
Fs.readFile(path, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
log.error('Error loading licenses', err)
|
||||
|
|
|
@ -253,10 +253,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
const status = state.status
|
||||
|
||||
if (
|
||||
!(
|
||||
__RELEASE_CHANNEL__ === 'development' ||
|
||||
__RELEASE_CHANNEL__ === 'test'
|
||||
) &&
|
||||
!(__RELEASE_CHANNEL__ === 'development') &&
|
||||
status === UpdateStatus.UpdateReady
|
||||
) {
|
||||
this.props.dispatcher.setUpdateBannerVisibility(true)
|
||||
|
@ -302,8 +299,14 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
this.props.dispatcher.installGlobalLFSFilters(false)
|
||||
|
||||
setInterval(() => this.checkForUpdates(true), UpdateCheckInterval)
|
||||
this.checkForUpdates(true)
|
||||
// We only want to automatically check for updates on beta and prod
|
||||
if (
|
||||
__RELEASE_CHANNEL__ !== 'development' &&
|
||||
__RELEASE_CHANNEL__ !== 'test'
|
||||
) {
|
||||
setInterval(() => this.checkForUpdates(true), UpdateCheckInterval)
|
||||
this.checkForUpdates(true)
|
||||
}
|
||||
|
||||
log.info(`launching: ${getVersion()} (${getOS()})`)
|
||||
log.info(`execPath: '${process.execPath}'`)
|
||||
|
@ -555,14 +558,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
|
||||
private checkForUpdates(inBackground: boolean) {
|
||||
if (__LINUX__) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
__RELEASE_CHANNEL__ === 'development' ||
|
||||
__RELEASE_CHANNEL__ === 'test'
|
||||
) {
|
||||
if (__LINUX__ || __RELEASE_CHANNEL__ === 'development') {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ interface IRange {
|
|||
}
|
||||
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
|
||||
interface IAutocompletingTextInputProps<ElementType> {
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@ import { IMatches } from '../../lib/fuzzy-find'
|
|||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import { dragAndDropManager } from '../../lib/drag-and-drop-manager'
|
||||
import { DragType, DropTargetType } from '../../models/drag-drop'
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
import { CommitMessage } from './commit-message'
|
||||
import { ChangedFile } from './changed-file'
|
||||
import { IAutocompletionProvider } from '../autocompletion'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { arrayEquals } from '../../lib/equality'
|
||||
import { clipboard } from 'electron'
|
||||
import { basename } from 'path'
|
||||
|
|
|
@ -27,7 +27,7 @@ import { CommitWarning, CommitWarningIcon } from './commit-warning'
|
|||
import { LinkButton } from '../lib/link-button'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
import { IAvatarUser, getAvatarUserFromAuthor } from '../../models/avatar'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { Account } from '../../models/account'
|
||||
import { CommitMessageAvatar } from './commit-message-avatar'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
|
|
|
@ -69,8 +69,8 @@ export class ModifiedImageDiff extends React.Component<
|
|||
super(props)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === this.container) {
|
||||
for (const { target, contentRect } of entries) {
|
||||
if (target === this.container && target instanceof HTMLElement) {
|
||||
// We might end up causing a recursive update by updating the state
|
||||
// when we're reacting to a resize so we'll defer it until after
|
||||
// react is done with this frame.
|
||||
|
@ -80,8 +80,8 @@ export class ModifiedImageDiff extends React.Component<
|
|||
|
||||
this.resizedTimeoutID = setImmediate(
|
||||
this.onResized,
|
||||
entry.target,
|
||||
entry.contentRect
|
||||
target,
|
||||
contentRect
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ import {
|
|||
getLineWidthFromDigitCount,
|
||||
getNumberOfDigits,
|
||||
} from './diff-helpers'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { getTokens } from './diff-syntax-mode'
|
||||
import { DiffSearchInput } from './diff-search-input'
|
||||
import { escapeRegExp } from '../../lib/helpers/regex'
|
||||
|
|
|
@ -40,7 +40,7 @@ import { structuralEquals } from '../../lib/equality'
|
|||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { clamp } from '../../lib/clamp'
|
||||
import { uuid } from '../../lib/uuid'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import {
|
||||
canSelect,
|
||||
|
|
|
@ -6,7 +6,7 @@ import { RichText } from '../lib/rich-text'
|
|||
import { RelativeTime } from '../relative-time'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { clipboard } from 'electron'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { CommitAttribution } from '../lib/commit-attribution'
|
||||
import { AvatarStack } from '../lib/avatar-stack'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
|
|
|
@ -23,7 +23,7 @@ import { ThrottledScheduler } from '../lib/throttled-scheduler'
|
|||
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { Resizable } from '../resizable'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
|
||||
import { CommitSummary } from './commit-summary'
|
||||
import { FileList } from './file-list'
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import * as remote from '@electron/remote'
|
||||
import { getPath } from '../main-process-proxy'
|
||||
import { getAppPathProxy } from '../main-process-proxy'
|
||||
|
||||
let app: Electron.App | null = null
|
||||
let path: string | null = null
|
||||
let userDataPath: string | null = null
|
||||
let documentsPath: string | null = null
|
||||
|
||||
export type PathType =
|
||||
|
@ -24,14 +22,6 @@ export type PathType =
|
|||
| 'logs'
|
||||
| 'crashDumps'
|
||||
|
||||
function getApp(): Electron.App {
|
||||
if (!app) {
|
||||
app = remote.app
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the app.
|
||||
*
|
||||
|
@ -53,27 +43,14 @@ export function getName(): string {
|
|||
*
|
||||
* This is preferable to using `remote` directly because we cache the result.
|
||||
*/
|
||||
export function getAppPath(): string {
|
||||
export async function getAppPath(): Promise<string> {
|
||||
if (!path) {
|
||||
path = getApp().getAppPath()
|
||||
path = await getAppPathProxy()
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the user's data.
|
||||
*
|
||||
* This is preferable to using `remote` directly because we cache the result.
|
||||
*/
|
||||
export function getUserDataPath(): string {
|
||||
if (!userDataPath) {
|
||||
userDataPath = getApp().getPath('userData')
|
||||
}
|
||||
|
||||
return userDataPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the user's documents path.
|
||||
*
|
||||
|
|
|
@ -13,7 +13,7 @@ import { arrayEquals } from '../../lib/equality'
|
|||
import { syncClockwise } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { IAuthor } from '../../models/author'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import { getLegacyStealthEmailForUser } from '../../lib/email'
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { join } from 'path'
|
||||
import { Repository } from '../../../models/repository'
|
||||
import { Dispatcher } from '../../dispatcher'
|
||||
import { showContextualMenu } from '../../main-process-proxy'
|
||||
import { showContextualMenu } from '../../../lib/menu-item'
|
||||
import { Octicon } from '../../octicons'
|
||||
import * as OcticonSymbol from '../../octicons/octicons.generated'
|
||||
import { PathText } from '../path-text'
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import * as React from 'react'
|
||||
|
||||
// broken for SFCs: https://github.com/Microsoft/tslint-microsoft-contrib/issues/339
|
||||
/* tslint:disable react-unused-props-and-state */
|
||||
|
||||
interface IHighlightTextProps {
|
||||
/** The text to render */
|
||||
readonly text: string
|
||||
|
|
|
@ -288,8 +288,8 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
|
||||
if (ResizeObserver || false) {
|
||||
this.resizeObserver = new ResizeObserverClass(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === this.list) {
|
||||
for (const { target, contentRect } of entries) {
|
||||
if (target === this.list && this.list !== null) {
|
||||
// We might end up causing a recursive update by updating the state
|
||||
// when we're reacting to a resize so we'll defer it until after
|
||||
// react is done with this frame.
|
||||
|
@ -299,8 +299,8 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
|
||||
this.updateSizeTimeoutId = setImmediate(
|
||||
this.onResized,
|
||||
entry.target,
|
||||
entry.contentRect
|
||||
this.list,
|
||||
contentRect
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
|
||||
interface ITextAreaProps {
|
||||
/** The label for the textarea field. */
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import classNames from 'classnames'
|
||||
import { createUniqueId, releaseUniqueId } from './id-pool'
|
||||
import { LinkButton } from './link-button'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
|
||||
export interface ITextBoxProps {
|
||||
/** The label for the input field. */
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import * as remote from '@electron/remote'
|
||||
import { ExecutableMenuItem } from '../models/app-menu'
|
||||
import { IMenuItem, ISerializableMenuItem } from '../lib/menu-item'
|
||||
import { RequestResponseChannels, RequestChannels } from '../lib/ipc-shared'
|
||||
import * as ipcRenderer from '../lib/ipc-renderer'
|
||||
|
||||
|
@ -184,6 +182,11 @@ export const getPath = invokeProxy('get-path', 1)
|
|||
*/
|
||||
export const getAppArchitecture = invokeProxy('get-app-architecture', 0)
|
||||
|
||||
/**
|
||||
* Tell the main process to obtain the application's app path
|
||||
*/
|
||||
export const getAppPathProxy = invokeProxy('get-app-path', 0)
|
||||
|
||||
/**
|
||||
* Tell the main process to obtain whether the app is running under a rosetta
|
||||
* translation
|
||||
|
@ -220,176 +223,7 @@ export const moveToApplicationsFolder = sendProxy(
|
|||
*/
|
||||
export const getAppMenu = sendProxy('get-app-menu', 0)
|
||||
|
||||
function findSubmenuItem(
|
||||
currentContextualMenuItems: ReadonlyArray<IMenuItem>,
|
||||
indices: ReadonlyArray<number>
|
||||
): IMenuItem | undefined {
|
||||
let foundMenuItem: IMenuItem | undefined = {
|
||||
submenu: currentContextualMenuItems,
|
||||
}
|
||||
|
||||
// Traverse the submenus of the context menu until we find the appropriate index.
|
||||
for (const index of indices) {
|
||||
if (foundMenuItem === undefined || foundMenuItem.submenu === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
foundMenuItem = foundMenuItem.submenu[index]
|
||||
}
|
||||
|
||||
return foundMenuItem
|
||||
}
|
||||
|
||||
let deferredContextMenuItems: ReadonlyArray<IMenuItem> | null = null
|
||||
|
||||
/** Takes a context menu and spelling suggestions from electron and merges them
|
||||
* into one context menu. */
|
||||
function mergeDeferredContextMenuItems(
|
||||
event: Electron.Event,
|
||||
params: Electron.ContextMenuParams
|
||||
) {
|
||||
if (deferredContextMenuItems === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const items = [...deferredContextMenuItems]
|
||||
const { misspelledWord, dictionarySuggestions } = params
|
||||
|
||||
if (!misspelledWord && dictionarySuggestions.length === 0) {
|
||||
showContextualMenu(items, false)
|
||||
return
|
||||
}
|
||||
|
||||
items.push({ type: 'separator' })
|
||||
|
||||
const { webContents } = remote.getCurrentWindow()
|
||||
|
||||
for (const suggestion of dictionarySuggestions) {
|
||||
items.push({
|
||||
label: suggestion,
|
||||
action: () => webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
}
|
||||
|
||||
if (misspelledWord) {
|
||||
items.push({
|
||||
label: __DARWIN__ ? 'Add to Dictionary' : 'Add to dictionary',
|
||||
action: () =>
|
||||
webContents.session.addWordToSpellCheckerDictionary(misspelledWord),
|
||||
})
|
||||
}
|
||||
|
||||
if (!__DARWIN__) {
|
||||
// NOTE: "On macOS as we use the native APIs there is no way to set the
|
||||
// language that the spellchecker uses" -- electron docs Therefore, we are
|
||||
// only allowing setting to English for non-mac machines.
|
||||
const spellCheckLanguageItem = getSpellCheckLanguageMenuItem(
|
||||
webContents.session
|
||||
)
|
||||
if (spellCheckLanguageItem !== null) {
|
||||
items.push(spellCheckLanguageItem)
|
||||
}
|
||||
}
|
||||
|
||||
showContextualMenu(items, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a menu item to give user the option to use English or their
|
||||
* system language.
|
||||
*
|
||||
* If system language is english, it returns null. If spellchecker is not set to
|
||||
* english, it returns item that can set it to English. If spellchecker is set
|
||||
* to english, it returns the item that can set it to their system language.
|
||||
*/
|
||||
function getSpellCheckLanguageMenuItem(
|
||||
session: Electron.session
|
||||
): IMenuItem | null {
|
||||
const userLanguageCode = remote.app.getLocale()
|
||||
const englishLanguageCode = 'en-US'
|
||||
const spellcheckLanguageCodes = session.getSpellCheckerLanguages()
|
||||
|
||||
if (
|
||||
userLanguageCode === englishLanguageCode &&
|
||||
spellcheckLanguageCodes.includes(englishLanguageCode)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const languageCode =
|
||||
spellcheckLanguageCodes.includes(englishLanguageCode) &&
|
||||
!spellcheckLanguageCodes.includes(userLanguageCode)
|
||||
? userLanguageCode
|
||||
: englishLanguageCode
|
||||
|
||||
const label =
|
||||
languageCode === englishLanguageCode
|
||||
? 'Set spellcheck to English'
|
||||
: 'Set spellcheck to system language'
|
||||
|
||||
return {
|
||||
label,
|
||||
action: () => session.setSpellCheckerLanguages([languageCode]),
|
||||
}
|
||||
}
|
||||
|
||||
const _showContextualMenu = invokeProxy('show-contextual-menu', 1)
|
||||
|
||||
/** Show the given menu items in a contextual menu. */
|
||||
export async function showContextualMenu(
|
||||
items: ReadonlyArray<IMenuItem>,
|
||||
mergeWithSpellcheckSuggestions = false
|
||||
) {
|
||||
/*
|
||||
When a user right clicks on a misspelled word in an input, we get event from
|
||||
electron. That event comes after the context menu event that we get from the
|
||||
dom. In order merge the spelling suggestions from electron with the context
|
||||
menu that the input wants to show, we stash the context menu items from the
|
||||
input away while we wait for the event from electron.
|
||||
*/
|
||||
if (deferredContextMenuItems !== null) {
|
||||
deferredContextMenuItems = null
|
||||
remote
|
||||
.getCurrentWebContents()
|
||||
.off('context-menu', mergeDeferredContextMenuItems)
|
||||
}
|
||||
|
||||
if (mergeWithSpellcheckSuggestions) {
|
||||
deferredContextMenuItems = items
|
||||
remote
|
||||
.getCurrentWebContents()
|
||||
.once('context-menu', mergeDeferredContextMenuItems)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
This is a regular context menu that does not need to merge with spellcheck
|
||||
items. They can be shown right away.
|
||||
*/
|
||||
const indices = await _showContextualMenu(serializeMenuItems(items))
|
||||
|
||||
if (indices !== null) {
|
||||
const menuItem = findSubmenuItem(items, indices)
|
||||
|
||||
if (menuItem !== undefined && menuItem.action !== undefined) {
|
||||
menuItem.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the menu items properties that can't be serializable in
|
||||
* order to pass them via IPC.
|
||||
*/
|
||||
function serializeMenuItems(
|
||||
items: ReadonlyArray<IMenuItem>
|
||||
): ReadonlyArray<ISerializableMenuItem> {
|
||||
return items.map(item => ({
|
||||
...item,
|
||||
action: undefined,
|
||||
submenu: item.submenu ? serializeMenuItems(item.submenu) : undefined,
|
||||
}))
|
||||
}
|
||||
export const invokeContextualMenu = invokeProxy('show-contextual-menu', 2)
|
||||
|
||||
/** Update the menu item labels with the user's preferred apps. */
|
||||
export const updatePreferredAppMenuItemLabels = sendProxy(
|
||||
|
|
|
@ -45,23 +45,13 @@ export interface IMultiCommitOperationProps {
|
|||
export abstract class BaseMultiCommitOperation extends React.Component<
|
||||
IMultiCommitOperationProps
|
||||
> {
|
||||
protected abstract onBeginOperation = () => {}
|
||||
|
||||
protected abstract onChooseBranch = (targetBranch: Branch) => {}
|
||||
|
||||
protected abstract onContinueAfterConflicts = async (): Promise<void> => {}
|
||||
|
||||
protected abstract onAbort = async (): Promise<void> => {}
|
||||
|
||||
protected abstract onConflictsDialogDismissed = () => {}
|
||||
|
||||
protected abstract renderChooseBranch = (): JSX.Element | null => {
|
||||
return null
|
||||
}
|
||||
|
||||
protected abstract renderCreateBranch = (): JSX.Element | null => {
|
||||
return null
|
||||
}
|
||||
protected abstract onBeginOperation: () => void
|
||||
protected abstract onChooseBranch: (targetBranch: Branch) => void
|
||||
protected abstract onContinueAfterConflicts: () => Promise<void>
|
||||
protected abstract onAbort: () => Promise<void>
|
||||
protected abstract onConflictsDialogDismissed: () => void
|
||||
protected abstract renderChooseBranch: () => JSX.Element | null
|
||||
protected abstract renderCreateBranch: () => JSX.Element | null
|
||||
|
||||
protected onFlowEnded = () => {
|
||||
this.props.dispatcher.closePopup(PopupType.MultiCommitOperation)
|
||||
|
|
|
@ -79,23 +79,17 @@ export abstract class BaseChooseBranchDialog extends React.Component<
|
|||
IBaseChooseBranchDialogProps,
|
||||
IBaseChooseBranchDialogState
|
||||
> {
|
||||
protected abstract start = () => {}
|
||||
protected abstract start: () => void
|
||||
|
||||
protected abstract canStart = (): boolean => {
|
||||
return false
|
||||
}
|
||||
protected abstract canStart: () => boolean
|
||||
|
||||
protected abstract updateStatus = async (branch: Branch) => {}
|
||||
protected abstract updateStatus: (branch: Branch) => Promise<void>
|
||||
|
||||
protected abstract getDialogTitle = (
|
||||
protected abstract getDialogTitle: (
|
||||
branchName: string
|
||||
): string | JSX.Element | undefined => {
|
||||
return branchName
|
||||
}
|
||||
) => string | JSX.Element | undefined
|
||||
|
||||
protected abstract renderActionStatusIcon = (): JSX.Element | null => {
|
||||
return null
|
||||
}
|
||||
protected abstract renderActionStatusIcon: () => JSX.Element | null
|
||||
|
||||
public constructor(props: IBaseChooseBranchDialogProps) {
|
||||
super(props)
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Dispatcher } from '../dispatcher'
|
|||
import { Button } from '../lib/button'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import { PopupType } from '../../models/popup'
|
||||
import { encodePathAsUrl } from '../../lib/path'
|
||||
|
|
|
@ -4,7 +4,7 @@ import { clipboard } from 'electron'
|
|||
import { Repository } from '../../models/repository'
|
||||
import { Octicon, iconForRepository } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { Repositoryish } from './group-repositories'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
|
|
68
app/src/ui/tab-bar-item.tsx
Normal file
68
app/src/ui/tab-bar-item.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import * as React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { TabBarType } from './tab-bar-type'
|
||||
|
||||
interface ITabBarItemProps {
|
||||
readonly index: number
|
||||
readonly selected: boolean
|
||||
readonly onClick: (index: number) => void
|
||||
readonly onMouseEnter: (index: number) => void
|
||||
readonly onMouseLeave: () => void
|
||||
readonly onSelectAdjacent: (
|
||||
direction: 'next' | 'previous',
|
||||
index: number
|
||||
) => void
|
||||
readonly onButtonRef: (
|
||||
index: number,
|
||||
button: HTMLButtonElement | null
|
||||
) => void
|
||||
readonly type?: TabBarType
|
||||
}
|
||||
|
||||
export class TabBarItem extends React.Component<ITabBarItemProps, {}> {
|
||||
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
this.props.onClick(this.props.index)
|
||||
}
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
const { type, index } = this.props
|
||||
const previousKey = type === TabBarType.Vertical ? 'ArrowUp' : 'ArrowLeft'
|
||||
const nextKey = type === TabBarType.Vertical ? 'ArrowDown' : 'ArrowRight'
|
||||
if (event.key === previousKey) {
|
||||
this.props.onSelectAdjacent('previous', index)
|
||||
event.preventDefault()
|
||||
} else if (event.key === nextKey) {
|
||||
this.props.onSelectAdjacent('next', index)
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
private onButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||
this.props.onButtonRef(this.props.index, buttonRef)
|
||||
}
|
||||
|
||||
private onMouseEnter = () => {
|
||||
this.props.onMouseEnter(this.props.index)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const selected = this.props.selected
|
||||
const className = classNames('tab-bar-item', { selected })
|
||||
return (
|
||||
<button
|
||||
ref={this.onButtonRef}
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
tabIndex={selected ? undefined : -1}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}
|
||||
type="button"
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
11
app/src/ui/tab-bar-type.ts
Normal file
11
app/src/ui/tab-bar-type.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/** The tab bar type. */
|
||||
export enum TabBarType {
|
||||
/** Standard tabs */
|
||||
Tabs,
|
||||
|
||||
/** Simpler switch appearance */
|
||||
Switch,
|
||||
|
||||
/** Vertical tabs */
|
||||
Vertical,
|
||||
}
|
|
@ -1,22 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
|
||||
import { TabBarItem } from './tab-bar-item'
|
||||
import { TabBarType } from './tab-bar-type'
|
||||
export { TabBarType } from './tab-bar-type'
|
||||
|
||||
/** Time to wait for drag element hover before switching tabs */
|
||||
const dragTabSwitchWaitTime = 500
|
||||
|
||||
/** The tab bar type. */
|
||||
export enum TabBarType {
|
||||
/** Standard tabs */
|
||||
Tabs,
|
||||
|
||||
/** Simpler switch appearance */
|
||||
Switch,
|
||||
|
||||
/** Vertical tabs */
|
||||
Vertical,
|
||||
}
|
||||
|
||||
interface ITabBarProps {
|
||||
/** The currently selected tab. */
|
||||
readonly selectedIndex: number
|
||||
|
@ -144,68 +134,3 @@ export class TabBar extends React.Component<ITabBarProps, {}> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface ITabBarItemProps {
|
||||
readonly index: number
|
||||
readonly selected: boolean
|
||||
readonly onClick: (index: number) => void
|
||||
readonly onMouseEnter: (index: number) => void
|
||||
readonly onMouseLeave: () => void
|
||||
readonly onSelectAdjacent: (
|
||||
direction: 'next' | 'previous',
|
||||
index: number
|
||||
) => void
|
||||
readonly onButtonRef: (
|
||||
index: number,
|
||||
button: HTMLButtonElement | null
|
||||
) => void
|
||||
readonly type?: TabBarType
|
||||
}
|
||||
|
||||
class TabBarItem extends React.Component<ITabBarItemProps, {}> {
|
||||
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
this.props.onClick(this.props.index)
|
||||
}
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
const { type, index } = this.props
|
||||
const previousKey = type === TabBarType.Vertical ? 'ArrowUp' : 'ArrowLeft'
|
||||
const nextKey = type === TabBarType.Vertical ? 'ArrowDown' : 'ArrowRight'
|
||||
if (event.key === previousKey) {
|
||||
this.props.onSelectAdjacent('previous', index)
|
||||
event.preventDefault()
|
||||
} else if (event.key === nextKey) {
|
||||
this.props.onSelectAdjacent('next', index)
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
private onButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||
this.props.onButtonRef(this.props.index, buttonRef)
|
||||
}
|
||||
|
||||
private onMouseEnter = () => {
|
||||
this.props.onMouseEnter(this.props.index)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const selected = this.props.selected
|
||||
const className = classNames('tab-bar-item', { selected })
|
||||
return (
|
||||
<button
|
||||
ref={this.onButtonRef}
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
tabIndex={selected ? undefined : -1}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}
|
||||
type="button"
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { PopupType } from '../../models/popup'
|
|||
import { PreferencesTab } from '../../models/preferences'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { suggestedExternalEditor } from '../../lib/editors/shared'
|
||||
import { TutorialStepInstructions } from './tutorial-step-instruction'
|
||||
|
||||
const TutorialPanelImage = encodePathAsUrl(
|
||||
__dirname,
|
||||
|
@ -310,93 +311,6 @@ export class TutorialPanel extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
interface ITutorialStepInstructionsProps {
|
||||
/** Text displayed to summarize this step */
|
||||
readonly summaryText: string
|
||||
/** Used to find out if this step has been completed */
|
||||
readonly isComplete: (step: ValidTutorialStep) => boolean
|
||||
/** The step for this section */
|
||||
readonly sectionId: ValidTutorialStep
|
||||
/** Used to find out if this is the next step for the user to complete */
|
||||
readonly isNextStepTodo: (step: ValidTutorialStep) => boolean
|
||||
|
||||
/** ID of the currently expanded tutorial step
|
||||
* (used to determine if this step is expanded)
|
||||
*/
|
||||
readonly currentlyOpenSectionId: ValidTutorialStep
|
||||
|
||||
/** Skip button (if possible for this step) */
|
||||
readonly skipLinkButton?: JSX.Element
|
||||
/** Handler to open and close section */
|
||||
readonly onSummaryClick: (id: ValidTutorialStep) => void
|
||||
}
|
||||
|
||||
/** A step (summary and expandable description) in the tutorial side panel */
|
||||
class TutorialStepInstructions extends React.Component<
|
||||
ITutorialStepInstructionsProps
|
||||
> {
|
||||
public render() {
|
||||
return (
|
||||
<li key={this.props.sectionId} onClick={this.onSummaryClick}>
|
||||
<details
|
||||
open={this.props.sectionId === this.props.currentlyOpenSectionId}
|
||||
onClick={this.onSummaryClick}
|
||||
>
|
||||
{this.renderSummary()}
|
||||
<div className="contents">{this.props.children}</div>
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSummary = () => {
|
||||
const shouldShowSkipLink =
|
||||
this.props.skipLinkButton !== undefined &&
|
||||
this.props.currentlyOpenSectionId === this.props.sectionId &&
|
||||
this.props.isNextStepTodo(this.props.sectionId)
|
||||
return (
|
||||
<summary>
|
||||
{this.renderTutorialStepIcon()}
|
||||
<span className="summary-text">{this.props.summaryText}</span>
|
||||
<span className="hang-right">
|
||||
{shouldShowSkipLink ? (
|
||||
this.props.skipLinkButton
|
||||
) : (
|
||||
<Octicon symbol={OcticonSymbol.chevronDown} />
|
||||
)}
|
||||
</span>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTutorialStepIcon() {
|
||||
if (this.props.isComplete(this.props.sectionId)) {
|
||||
return (
|
||||
<div className="green-circle">
|
||||
<Octicon symbol={OcticonSymbol.check} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ugh zero-indexing
|
||||
const stepNumber = orderedTutorialSteps.indexOf(this.props.sectionId) + 1
|
||||
return this.props.isNextStepTodo(this.props.sectionId) ? (
|
||||
<div className="blue-circle">{stepNumber}</div>
|
||||
) : (
|
||||
<div className="empty-circle">{stepNumber}</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onSummaryClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
// prevents the default behavior of toggling on a `details` html element
|
||||
// so we don't have to fight it with our react state
|
||||
// for more info see:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#Events
|
||||
e.preventDefault()
|
||||
this.props.onSummaryClick(this.props.sectionId)
|
||||
}
|
||||
}
|
||||
|
||||
const SkipLinkButton: React.FunctionComponent<{
|
||||
onClick: () => void
|
||||
}> = props => <LinkButton onClick={props.onClick}>Skip</LinkButton>
|
||||
|
|
94
app/src/ui/tutorial/tutorial-step-instruction.tsx
Normal file
94
app/src/ui/tutorial/tutorial-step-instruction.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
ValidTutorialStep,
|
||||
orderedTutorialSteps,
|
||||
} from '../../models/tutorial-step'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
||||
interface ITutorialStepInstructionsProps {
|
||||
/** Text displayed to summarize this step */
|
||||
readonly summaryText: string
|
||||
/** Used to find out if this step has been completed */
|
||||
readonly isComplete: (step: ValidTutorialStep) => boolean
|
||||
/** The step for this section */
|
||||
readonly sectionId: ValidTutorialStep
|
||||
/** Used to find out if this is the next step for the user to complete */
|
||||
readonly isNextStepTodo: (step: ValidTutorialStep) => boolean
|
||||
|
||||
/** ID of the currently expanded tutorial step
|
||||
* (used to determine if this step is expanded)
|
||||
*/
|
||||
readonly currentlyOpenSectionId: ValidTutorialStep
|
||||
|
||||
/** Skip button (if possible for this step) */
|
||||
readonly skipLinkButton?: JSX.Element
|
||||
/** Handler to open and close section */
|
||||
readonly onSummaryClick: (id: ValidTutorialStep) => void
|
||||
}
|
||||
|
||||
/** A step (summary and expandable description) in the tutorial side panel */
|
||||
export class TutorialStepInstructions extends React.Component<
|
||||
ITutorialStepInstructionsProps
|
||||
> {
|
||||
public render() {
|
||||
return (
|
||||
<li key={this.props.sectionId} onClick={this.onSummaryClick}>
|
||||
<details
|
||||
open={this.props.sectionId === this.props.currentlyOpenSectionId}
|
||||
onClick={this.onSummaryClick}
|
||||
>
|
||||
{this.renderSummary()}
|
||||
<div className="contents">{this.props.children}</div>
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSummary = () => {
|
||||
const shouldShowSkipLink =
|
||||
this.props.skipLinkButton !== undefined &&
|
||||
this.props.currentlyOpenSectionId === this.props.sectionId &&
|
||||
this.props.isNextStepTodo(this.props.sectionId)
|
||||
return (
|
||||
<summary>
|
||||
{this.renderTutorialStepIcon()}
|
||||
<span className="summary-text">{this.props.summaryText}</span>
|
||||
<span className="hang-right">
|
||||
{shouldShowSkipLink ? (
|
||||
this.props.skipLinkButton
|
||||
) : (
|
||||
<Octicon symbol={OcticonSymbol.chevronDown} />
|
||||
)}
|
||||
</span>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTutorialStepIcon() {
|
||||
if (this.props.isComplete(this.props.sectionId)) {
|
||||
return (
|
||||
<div className="green-circle">
|
||||
<Octicon symbol={OcticonSymbol.check} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ugh zero-indexing
|
||||
const stepNumber = orderedTutorialSteps.indexOf(this.props.sectionId) + 1
|
||||
return this.props.isNextStepTodo(this.props.sectionId) ? (
|
||||
<div className="blue-circle">{stepNumber}</div>
|
||||
) : (
|
||||
<div className="empty-circle">{stepNumber}</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onSummaryClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
// prevents the default behavior of toggling on a `details` html element
|
||||
// so we don't have to fight it with our react state
|
||||
// for more info see:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#Events
|
||||
e.preventDefault()
|
||||
this.props.onSummaryClick(this.props.sectionId)
|
||||
}
|
||||
}
|
|
@ -3,8 +3,6 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
|||
import { WindowState } from '../../lib/window-state'
|
||||
|
||||
interface IFullScreenInfoProps {
|
||||
// react-unused-props-and-state doesn't understand getDerivedStateFromProps
|
||||
// tslint:disable-next-line:react-unused-props-and-state
|
||||
readonly windowState: WindowState | null
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# The least terrible way to resolve a symlink to its real path.
|
||||
function realpath() {
|
||||
/usr/bin/python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0";
|
||||
/usr/bin/perl -e "use Cwd;print Cwd::abs_path(@ARGV[0])" "$0";
|
||||
}
|
||||
|
||||
CONTENTS="$(dirname "$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")")"
|
||||
|
|
|
@ -33,11 +33,7 @@ const commonConfig: webpack.Configuration = {
|
|||
include: path.resolve(__dirname, 'src'),
|
||||
use: [
|
||||
{
|
||||
loader: 'awesome-typescript-loader',
|
||||
options: {
|
||||
useBabel: false,
|
||||
useCache: true,
|
||||
},
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
|
@ -220,14 +216,9 @@ highlighter.module!.rules = [
|
|||
include: path.resolve(__dirname, 'src/highlighter'),
|
||||
use: [
|
||||
{
|
||||
loader: 'awesome-typescript-loader',
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
useBabel: false,
|
||||
useCache: true,
|
||||
configFileName: path.resolve(
|
||||
__dirname,
|
||||
'src/highlighter/tsconfig.json'
|
||||
),
|
||||
configFile: path.resolve(__dirname, 'src/highlighter/tsconfig.json'),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
105
app/yarn.lock
105
app/yarn.lock
|
@ -306,6 +306,13 @@ decompress-response@^4.2.0:
|
|||
dependencies:
|
||||
mimic-response "^2.0.0"
|
||||
|
||||
decompress-response@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
|
||||
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
|
||||
dependencies:
|
||||
mimic-response "^3.1.0"
|
||||
|
||||
deep-equal@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
|
@ -326,6 +333,15 @@ delegates@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
desktop-notifications@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/desktop-notifications/-/desktop-notifications-0.1.4.tgz#698c5822fb88d7a2289f36b4f64eaac8b5bdefed"
|
||||
integrity sha512-3ADhNvqL9ISbN17x/hWcYUcGHvWJxPnoxxsQbmrU26wpk2eakvDplCm0fvNIpHx/Ar5O7n1VmMOVmLr8W6B8sQ==
|
||||
dependencies:
|
||||
node-addon-api "^4.3.0"
|
||||
prebuild-install "^7.0.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
desktop-trampoline@desktop/desktop-trampoline#v0.9.8:
|
||||
version "0.9.8"
|
||||
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/cbd3dbb31d0d3ea9f325067f48bfbf60b6663a57"
|
||||
|
@ -345,6 +361,11 @@ detect-libc@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||
|
||||
detect-libc@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204"
|
||||
integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw==
|
||||
|
||||
devtron@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/devtron/-/devtron-1.4.0.tgz#b5e748bd6e95bbe70bfcc68aae6fe696119441e1"
|
||||
|
@ -832,13 +853,13 @@ keyboardevents-areequal@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
||||
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
|
||||
|
||||
keytar@^7.7.0:
|
||||
version "7.7.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2"
|
||||
integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==
|
||||
keytar@^7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.8.0.tgz#28cf5ceeb1275350888870022869b8b4fe6a87f9"
|
||||
integrity sha512-mR+BqtAOIW8j+T5FtLVyckCbvROWQD+4FzPeFMuk5njEZkXLpVPCGF26Y3mTyxMAAL1XCfswR7S6kIf+THSRFA==
|
||||
dependencies:
|
||||
node-addon-api "^3.0.0"
|
||||
prebuild-install "^6.0.0"
|
||||
node-addon-api "^4.3.0"
|
||||
prebuild-install "^7.0.1"
|
||||
|
||||
keyv@^3.0.0:
|
||||
version "3.1.0"
|
||||
|
@ -888,6 +909,13 @@ lowercase-keys@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
|
||||
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
map-age-cleaner@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
|
||||
|
@ -934,6 +962,11 @@ mimic-response@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46"
|
||||
integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==
|
||||
|
||||
mimic-response@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
|
@ -1027,16 +1060,23 @@ node-abi@^2.7.0:
|
|||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-addon-api@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
|
||||
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
|
||||
node-abi@^3.3.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.7.0.tgz#ed980f6dbb6db9ff3b31aeb27d43cd9b096f6e9e"
|
||||
integrity sha512-3J+U4CvxVNEk9+lGdJkmYbN8cIN0HMTDT9R0ezX7pmp7aD6BaKsfAHwVn3IvVg6pYIRUuQ+gHW1eawrvywnSQQ==
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
node-addon-api@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
|
||||
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
|
||||
|
||||
node-addon-api@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
|
||||
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
|
@ -1199,6 +1239,25 @@ prebuild-install@^6.0.0:
|
|||
tunnel-agent "^0.6.0"
|
||||
which-pm-runs "^1.0.0"
|
||||
|
||||
prebuild-install@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"
|
||||
integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==
|
||||
dependencies:
|
||||
detect-libc "^2.0.0"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^3.3.0"
|
||||
npmlog "^4.0.1"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^4.0.0"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
prepend-http@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
|
@ -1466,6 +1525,13 @@ semver@^7.2.1:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
@ -1512,6 +1578,15 @@ simple-get@^3.0.3:
|
|||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
simple-get@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
|
||||
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
|
||||
dependencies:
|
||||
decompress-response "^6.0.0"
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
source-map-support@^0.4.15:
|
||||
version "0.4.18"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
|
||||
|
@ -1719,6 +1794,11 @@ uuid@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||
integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==
|
||||
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
warning@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
|
||||
|
@ -1816,3 +1896,8 @@ yallist@^3.0.0, yallist@^3.1.1:
|
|||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
{
|
||||
"releases": {
|
||||
"2.9.7-beta1": [
|
||||
"[New] Initial support for system notifications when checks fail in macOS - #13655",
|
||||
"[Added] Support pushing workflow files for GitHub Actions to GitHub Enterprise Server - #13640",
|
||||
"[Added] Support CLion as an external editor - #13739. Thanks @Pinzauti!",
|
||||
"[Fixed] Fix close button in full screen mode on macOS - #12838",
|
||||
"[Fixed] Commit message dialog background styles match dialog - #13606",
|
||||
"[Fixed] Ensure job steps on pull request check run list are always present - #13531",
|
||||
"[Improved] Take alias into account when sorting repositories - #13429",
|
||||
"[Improved] Upgrade to Electron v14.2.3 - #13689",
|
||||
"[Improved] Support avatars on GitHub Enterprise Server - #13719",
|
||||
"[Improved] Fetch before trying to follow a URL link to a specific branch - #13641. Thanks @Bestra!",
|
||||
"[Improved] Add \"View on GitHub\" context menu option to repository list items - #13227. Thanks @lhvy!",
|
||||
"[Improved] Signal when a commit summary is getting long - #2055. Thanks @Twixes!",
|
||||
"[Improved] Check run group headers and checks stay in view while scrolling the sub checks or job steps. - #13532",
|
||||
"[Improved] Remove unnecessary punctuation in appearance settings- #13715. Thanks @Pinzauti!"
|
||||
],
|
||||
"2.9.6": [
|
||||
"[Added] View and re-run the check runs for the checked out pull request.",
|
||||
"[Fixed] Tooltip improvements and polish - #13452 #13449",
|
||||
|
|
|
@ -8,7 +8,6 @@ Our linting tooling uses a combination of
|
|||
|
||||
* [Prettier](https://github.com/prettier/prettier)
|
||||
* [ESLint](https://github.com/eslint/eslint)
|
||||
* [TSLint](https://github.com/palantir/tslint).
|
||||
|
||||
Most (if not all) editors have integrations for these tools so that they will report errors and fix formatting in realtime. See [tooling](./tooling.md) for how to set these integrations up while developing for desktop.
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# TypeScript Style Guide
|
||||
|
||||
Most of our preferred style when writing typescript is configured in our [`tslint.json`](../../tslint.json) and [`.eslintrc.yml`](../../.eslintrc.yml) files.
|
||||
Most of our preferred style when writing typescript is configured in our [`.eslintrc.yml`](../../.eslintrc.yml) files.
|
||||
|
||||
## Do
|
||||
- Use camelCase for methods
|
||||
- Use PascalCase for class names
|
||||
- Enable [TSLint](https://palantir.github.io/tslint/usage/third-party-tools/) and [ESLint](https://eslint.org/docs/user-guide/integrations) in your editor
|
||||
- [ESLint](https://eslint.org/docs/user-guide/integrations) in your editor
|
||||
|
||||
# Documenting your code
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ Recommended packages:
|
|||
* [build-npm-apm](https://atom.io/packages/build-npm-apm) - invoke
|
||||
all npm scripts straight from the editor by pressing F7 (requires
|
||||
[build](https://atom.io/packages/build))
|
||||
* [linter](https://atom.io/packages/linter) and
|
||||
[linter-tslint](https://atom.io/packages/linter-tslint) - shows linter errors and warning in the editor
|
||||
* [linter](https://atom.io/packages/linter) - shows linter errors and warning in the editor
|
||||
|
||||
You can install them all at once with:
|
||||
|
||||
|
|
26
package.json
26
package.json
|
@ -29,9 +29,8 @@
|
|||
"lint": "yarn prettier && yarn lint:src",
|
||||
"lint:fix": "yarn prettier --write && yarn lint:src:fix",
|
||||
"prettier": "prettier --check \"./**/*.{ts,tsx,js,json,jsx,scss,html,yaml,yml}\"",
|
||||
"lint:src": "yarn tslint && yarn eslint-check && yarn eslint",
|
||||
"lint:src:fix": "yarn tslint --fix && yarn eslint --fix",
|
||||
"tslint": "tslint -p .",
|
||||
"lint:src": "yarn eslint-check && yarn eslint",
|
||||
"lint:src:fix": "yarn eslint --fix",
|
||||
"eslint": "eslint --cache --rulesdir ./eslint-rules \"./eslint-rules/**/*.js\" \"./script/**/*.ts{,x}\" \"./app/{src,typings,test}/**/*.{j,t}s{,x}\" \"./changelog.json\"",
|
||||
"eslint-check": "eslint --print-config .eslintrc.* | eslint-config-prettier-check",
|
||||
"publish": "ts-node -P script/tsconfig.json script/publish.ts",
|
||||
|
@ -60,14 +59,13 @@
|
|||
"@types/marked": "^4.0.1",
|
||||
"@types/plist": "^3.0.2",
|
||||
"@types/react-color": "^3.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^3.5.0",
|
||||
"@typescript-eslint/experimental-utils": "^3.5.0",
|
||||
"@typescript-eslint/parser": "^3.5.0",
|
||||
"@typescript-eslint/typescript-estree": "^3.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/experimental-utils": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@typescript-eslint/typescript-estree": "^5.10.2",
|
||||
"airbnb-browser-shims": "^3.0.0",
|
||||
"ajv": "^6.4.0",
|
||||
"awesome-node-loader": "^1.1.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"azure-storage": "^2.10.4",
|
||||
"chalk": "^2.2.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
|
@ -76,11 +74,10 @@
|
|||
"css-loader": "^2.1.0",
|
||||
"eslint": "^7.3.1",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-jsdoc": "^28.6.1",
|
||||
"eslint-plugin-jsdoc": "^37.7.0",
|
||||
"eslint-plugin-json": "^2.1.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react": "7.26.1",
|
||||
"express": "^4.15.0",
|
||||
"fake-indexeddb": "^2.0.4",
|
||||
"file-loader": "^2.0.0",
|
||||
|
@ -108,12 +105,9 @@
|
|||
"style-loader": "^0.21.0",
|
||||
"to-camel-case": "^1.0.0",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-loader": "^8",
|
||||
"ts-node": "^7.0.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-prettier": "^1.14.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"typescript": "^3.9.5",
|
||||
"typescript-tslint-plugin": "^0.0.6",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^4.8.3",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-dev-middleware": "^3.1.3",
|
||||
|
|
|
@ -273,60 +273,27 @@ function moveAnalysisFiles() {
|
|||
}
|
||||
|
||||
function copyDependencies() {
|
||||
const originalPackage: Package = require(path.join(
|
||||
projectRoot,
|
||||
'app',
|
||||
'package.json'
|
||||
))
|
||||
const pkg: Package = require(path.join(projectRoot, 'app', 'package.json'))
|
||||
|
||||
const oldDependencies = originalPackage.dependencies
|
||||
const newDependencies: PackageLookup = {}
|
||||
|
||||
for (const name of Object.keys(oldDependencies)) {
|
||||
const spec = oldDependencies[name]
|
||||
if (externals.indexOf(name) !== -1) {
|
||||
newDependencies[name] = spec
|
||||
}
|
||||
}
|
||||
|
||||
const oldDevDependencies = originalPackage.devDependencies
|
||||
const newDevDependencies: PackageLookup = {}
|
||||
|
||||
if (isDevelopmentBuild) {
|
||||
for (const name of Object.keys(oldDevDependencies)) {
|
||||
const spec = oldDevDependencies[name]
|
||||
if (externals.indexOf(name) !== -1) {
|
||||
newDevDependencies[name] = spec
|
||||
}
|
||||
}
|
||||
}
|
||||
const filterExternals = (dependencies: Record<string, string>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(dependencies).filter(([k]) => externals.includes(k))
|
||||
)
|
||||
|
||||
// The product name changes depending on whether it's a prod build or dev
|
||||
// build, so that we can have them running side by side.
|
||||
const updatedPackage = Object.assign({}, originalPackage, {
|
||||
productName: getProductName(),
|
||||
dependencies: newDependencies,
|
||||
devDependencies: newDevDependencies,
|
||||
})
|
||||
|
||||
if (!isDevelopmentBuild) {
|
||||
delete updatedPackage.devDependencies
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outRoot, 'package.json'),
|
||||
JSON.stringify(updatedPackage)
|
||||
)
|
||||
pkg.productName = getProductName()
|
||||
pkg.dependencies = filterExternals(pkg.dependencies)
|
||||
pkg.devDependencies =
|
||||
isDevelopmentBuild && pkg.devDependencies
|
||||
? filterExternals(pkg.devDependencies)
|
||||
: {}
|
||||
|
||||
fs.writeFileSync(path.join(outRoot, 'package.json'), JSON.stringify(pkg))
|
||||
fs.removeSync(path.resolve(outRoot, 'node_modules'))
|
||||
|
||||
if (
|
||||
Object.keys(newDependencies).length ||
|
||||
Object.keys(newDevDependencies).length
|
||||
) {
|
||||
console.log(' Installing dependencies via yarn…')
|
||||
cp.execSync('yarn install', { cwd: outRoot, env: process.env })
|
||||
}
|
||||
console.log(' Installing dependencies via yarn…')
|
||||
cp.execSync('yarn install', { cwd: outRoot, env: process.env })
|
||||
|
||||
console.log(' Copying desktop-trampoline…')
|
||||
const desktopTrampolineDir = path.resolve(outRoot, 'desktop-trampoline')
|
||||
|
|
|
@ -120,7 +120,7 @@ export async function run(args: ReadonlyArray<string>): Promise<void> {
|
|||
console.log(`Set!`)
|
||||
} catch (e) {
|
||||
console.warn(`Setting the app version failed 😿
|
||||
(${e.message})
|
||||
(${e instanceof Error ? e.message : e})
|
||||
Please manually set it to ${nextVersion} in app/package.json.`)
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,11 @@ export async function run(args: ReadonlyArray<string>): Promise<void> {
|
|||
console.log('Added!')
|
||||
printInstructions(nextVersion, [])
|
||||
} catch (e) {
|
||||
console.warn(`Writing the changelog failed 😿\n(${e.message})`)
|
||||
console.warn(
|
||||
`Writing the changelog failed 😿\n(${
|
||||
e instanceof Error ? e.message : e
|
||||
})`
|
||||
)
|
||||
printInstructions(nextVersion, newEntries)
|
||||
}
|
||||
} else {
|
||||
|
|
8
script/globals.d.ts
vendored
8
script/globals.d.ts
vendored
|
@ -1,9 +1,7 @@
|
|||
// type annotations for package.json dependencies
|
||||
type PackageLookup = { [key: string]: string }
|
||||
|
||||
type Package = {
|
||||
dependencies: PackageLookup
|
||||
devDependencies: PackageLookup
|
||||
productName?: string
|
||||
dependencies: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
declare namespace NodeJS {
|
||||
|
|
|
@ -14,15 +14,10 @@
|
|||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"outDir": "./out",
|
||||
"lib": ["ES2017", "DOM", "DOM.Iterable", "ES2018.Promise", "ES2020.string"]
|
||||
"lib": ["ES2017", "DOM", "DOM.Iterable", "ES2018.Promise", "ES2020.string"],
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-tslint-plugin"
|
||||
}
|
||||
],
|
||||
"include": ["app/**/*.ts", "app/**/*.tsx", "app/**/*.d.tsx"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
15
tslint.json
15
tslint.json
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"extends": ["tslint-config-prettier"],
|
||||
"rulesDirectory": ["node_modules/tslint-microsoft-contrib/"],
|
||||
"rules": {
|
||||
"promise-must-complete": true,
|
||||
"react-unused-props-and-state": [
|
||||
true,
|
||||
{
|
||||
"props-interface-regex": "Props$",
|
||||
"state-interface-regex": "State$"
|
||||
}
|
||||
],
|
||||
"react-this-binding-issue": true
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue