This commit is contained in:
crusader 2018-08-17 15:11:55 +09:00
parent 66bb90dda6
commit 7c915f6d5f
23 changed files with 1578 additions and 96 deletions

10
@overflow/core/merge.ts Normal file
View File

@ -0,0 +1,10 @@
/** Create a copy of an object by merging it with a subset of its properties. */
export function merge<T, K extends keyof T>(obj: T, subset: Pick<T, K>): T {
const copy = Object.assign({}, obj);
for (const k in subset) {
if (subset[k]) {
copy[k] = subset[k];
}
}
return copy;
}

View File

@ -39,9 +39,10 @@
"@ngrx/schematics": "^6.1.0", "@ngrx/schematics": "^6.1.0",
"@ngrx/store": "^6.1.0", "@ngrx/store": "^6.1.0",
"@ngrx/store-devtools": "^6.1.0", "@ngrx/store-devtools": "^6.1.0",
"@types/fs-extra": "^5.0.4",
"@types/jasmine": "~2.8.6", "@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4", "@types/node": "^8.10.4",
"awesome-typescript-loader": "^5.2.0", "awesome-typescript-loader": "^5.2.0",
"codelyzer": "~4.2.1", "codelyzer": "~4.2.1",
"core-js": "^2.5.4", "core-js": "^2.5.4",

View File

@ -2,7 +2,7 @@
<div class="ui-g"> <div class="ui-g">
<div class="ui-g-12 ui-nopad"> <div class="ui-g-12 ui-nopad">
<div class="card"> <div class="card">
<h1>Home works!!</h1> <h1>Home works!!!!</h1>
<p-panel #content [showHeader]="false" class="block-panel"> <p-panel #content [showHeader]="false" class="block-panel">
<div class="ui-g" dir="rtl"> <div class="ui-g" dir="rtl">
<button class="ui-button-width-fit" type="button" label="Discovery" icon="ui-icon-search" pButton></button> <button class="ui-button-width-fit" type="button" label="Discovery" icon="ui-icon-search" pButton></button>

View File

@ -0,0 +1,665 @@
/** A type union of all possible types of menu items */
export type MenuItem =
| IMenuItem
| ISubmenuItem
| ISeparatorMenuItem
| ICheckboxMenuItem
| IRadioMenuItem;
/** A type union of all types of menu items which can be executed */
export type ExecutableMenuItem = IMenuItem | ICheckboxMenuItem | IRadioMenuItem;
/**
* Common properties for all item types except separator.
* Only useful for declaring the types, not for consumption
*/
interface IBaseMenuItem {
readonly id: string;
readonly enabled: boolean;
readonly visible: boolean;
readonly label: string;
}
/**
* An interface describing the properties of a 'normal'
* menu item, i.e. a clickable item with a label but no
* other special properties.
*/
export interface IMenuItem extends IBaseMenuItem {
readonly type: 'menuItem';
readonly accelerator: string | null;
readonly accessKey: string | null;
}
/**
* An interface describing the properties of a
* submenu menu item, i.e. an item which has an associated
* submenu which can be expanded to reveal more menu
* item. Not in itself executable, only a container.
*/
export interface ISubmenuItem extends IBaseMenuItem {
readonly type: 'submenuItem';
readonly menu: IMenu;
readonly accessKey: string | null;
}
/**
* An interface describing the properties of a checkbox
* menu item, i.e. an item which has an associated checked
* state that can be toggled by executing it.
*/
export interface ICheckboxMenuItem extends IBaseMenuItem {
readonly type: 'checkbox';
readonly accelerator: string | null;
readonly accessKey: string | null;
readonly checked: boolean;
}
/**
* An interface describing the properties of a checkbox
* menu item, i.e. an item which has an associated checked
* state that is checked or unchecked based on application logic.
*
* The radio menu item is probably going to be used in a collection
* of more radio menu items where the checked item is assigned
* based on the last executed item in that group.
*/
export interface IRadioMenuItem extends IBaseMenuItem {
readonly type: 'radio';
readonly accelerator: string | null;
readonly accessKey: string | null;
readonly checked: boolean;
}
/**
* An interface describing the properties of a separator menu
* item, i.e. an item which sole purpose is to create separation
* between menu items. It has no other semantics and is purely
* a visual hint.
*/
export interface ISeparatorMenuItem {
readonly id: string;
readonly type: 'separator';
readonly visible: boolean;
}
/**
* An interface describing a menu.
*
* Holds collection of menu items and an indication of which item (if any)
* in the menu is selected.
*/
export interface IMenu {
/**
* The id of this menu. For the root menu this will be undefined. For all
* other menus it will be the same as the id of the submenu item which
* owns this menu.
*
* +---------------------------+
* | Root menu (id: undefined) |
* +---------------------------+ +--------------------------+
* | File (id File) +--> File menu (id: File) |
* +---------------------------+ +--------------------------+
* | Edit (id Edit) | | Open (id File.Open) |
* +---------------------------+ +--------------------------+
* | Close (id File.Close) |
* +--------------------------+
*/
readonly id?: string;
/** Type identifier, used for type narrowing */
readonly type: 'menu';
/** A collection of zero or more menu items */
readonly items: ReadonlyArray<MenuItem>;
/** The selected item in the menu or undefined if no item is selected */
readonly selectedItem?: MenuItem;
}
/**
* Gets the accelerator for a given menu item. If the menu item doesn't
* have an explicitly defined accelerator but does have a defined role
* the default accelerator (if any) for that particular role will be
* returned.
*/
function getAccelerator(menuItem: Electron.MenuItem): string | null {
if (menuItem.accelerator) {
return menuItem.accelerator as string;
}
if (menuItem.role) {
const unsafeItem = menuItem as any;
// https://github.com/electron/electron/blob/d4a8a64ba/lib/browser/api/menu-item.js#L62
const getDefaultRoleAccelerator = unsafeItem.getDefaultRoleAccelerator;
if (typeof getDefaultRoleAccelerator === 'function') {
try {
const defaultRoleAccelerator = getDefaultRoleAccelerator.call(menuItem);
if (typeof defaultRoleAccelerator === 'string') {
return defaultRoleAccelerator;
}
} catch (err) {
console.error('Could not retrieve default accelerator', err);
}
}
}
return null;
}
/**
* Return the access key (applicable on Windows) from a menu item label.
*
* An access key is a letter or symbol preceded by an ampersand, i.e. in
* the string "Check for &updates" the access key is 'u'. Access keys are
* case insensitive and are unique per menu.
*/
function getAccessKey(text: string): string | null {
const m = text.match(/&([^&])/);
return m ? m[1] : null;
}
/**
* Creates an instance of one of the types in the MenuItem type union based
* on an Electron MenuItem instance. Will recurse through all sub menus and
* convert each item.
*/
function menuItemFromElectronMenuItem(menuItem: Electron.MenuItem): MenuItem {
// Our menu items always have ids and Electron.MenuItem takes on whatever
// properties was defined on the MenuItemOptions template used to create it
// but doesn't surface those in the type declaration.
const id: string | undefined = (menuItem as any).id;
if (!id) {
throw new Error(`menuItem must specify id: ${menuItem.label}`);
}
const enabled = menuItem.enabled;
const visible = menuItem.visible;
const label = menuItem.label;
const checked = menuItem.checked;
const accelerator = getAccelerator(menuItem);
const accessKey = getAccessKey(menuItem.label);
// normal, separator, submenu, checkbox or radio.
switch (menuItem.type) {
case 'normal':
return {
id,
type: 'menuItem',
label,
enabled,
visible,
accelerator,
accessKey,
};
case 'separator':
return { id, type: 'separator', visible };
case 'submenu':
const menu = menuFromElectronMenu(menuItem.submenu as Electron.Menu, id);
return {
id,
type: 'submenuItem',
label,
enabled,
visible,
menu,
accessKey,
};
case 'checkbox':
return {
id,
type: 'checkbox',
label,
enabled,
visible,
accelerator,
checked,
accessKey,
};
case 'radio':
return {
id,
type: 'radio',
label,
enabled,
visible,
accelerator,
checked,
accessKey,
};
default:
// return assertNever(
// menuItem.type,
// `Unknown menu item type ${menuItem.type}`
// );
}
}
/**
* Creates a IMenu instance based on an Electron Menu instance.
* Will recurse through all sub menus and convert each item using
* menuItemFromElectronMenuItem.
*
* @param menu - The electron menu instance to convert into an
* IMenu instance
*
* @param id - The id of the menu. Menus share their id with
* their parent item. The root menu id is undefined.
*/
export function menuFromElectronMenu(menu: Electron.Menu, id?: string): IMenu {
const items = menu.items.map(menuItemFromElectronMenuItem);
if (__DEV__) {
const seenAccessKeys = new Set<string>();
for (const item of items) {
if (item.visible) {
if (itemMayHaveAccessKey(item) && item.accessKey) {
if (seenAccessKeys.has(item.accessKey.toLowerCase())) {
throw new Error(
`Duplicate access key '${item.accessKey}' for item ${item.label}`
);
} else {
seenAccessKeys.add(item.accessKey.toLowerCase());
}
}
}
}
}
return { id, type: 'menu', items };
}
/**
* Creates a map between MenuItem ids and MenuItems by recursing
* through all items and all submenus.
*/
function buildIdMap(
menu: IMenu,
map = new Map<string, MenuItem>()
): Map<string, MenuItem> {
for (const item of menu.items) {
map.set(item.id, item);
if (item.type === 'submenuItem') {
buildIdMap(item.menu, map);
}
}
return map;
}
/** Type guard which narrows a MenuItem to one which supports access keys */
export function itemMayHaveAccessKey(
item: MenuItem
): item is IMenuItem | ISubmenuItem | ICheckboxMenuItem | IRadioMenuItem {
return (
item.type === 'menuItem' ||
item.type === 'submenuItem' ||
item.type === 'checkbox' ||
item.type === 'radio'
);
}
/**
* Returns a value indicating whether or not the given menu item can be
* selected. Selectable items are non-separator items which are enabled
* and visible.
*/
export function itemIsSelectable(item: MenuItem) {
return item.type !== 'separator' && item.enabled && item.visible;
}
/**
* Attempts to locate a menu item matching the provided access key in a
* given list of items. The access key comparison is case-insensitive.
*
* Note that this function does not take into account whether or not the
* item is selectable, consumers of this function need to perform that
* check themselves when applicable.
*/
export function findItemByAccessKey(
accessKey: string,
items: ReadonlyArray<MenuItem>
): IMenuItem | ISubmenuItem | ICheckboxMenuItem | IRadioMenuItem | null {
const lowerCaseAccessKey = accessKey.toLowerCase();
for (const item of items) {
if (itemMayHaveAccessKey(item)) {
if (
item.accessKey &&
item.accessKey.toLowerCase() === lowerCaseAccessKey
) {
return item;
}
}
}
return null;
}
/**
* An immutable, transformable object which represents an application menu
* and its current state (which menus are open, which items are selected).
*
* The primary use case for this is for rendering a custom application menu
* on non-macOS systems. As such some interactions are explicitly made to
* conform to Windows menu interactions. This includes things like selecting
* the entire path up until the last selected item. This is necessary since,
* on Windows, the parent menu item of a menu might not be selected even
* though the submenu is. This is in order to allow for some delay when
* moving the cursor from one menu pane to another.
*
* In general, however, this object is not platform specific and much of
* the interactions are defined by the component using it.
*/
export class AppMenu {
/**
* A list of currently open menus with their selected items
* in the application menu.
*
* The semantics around what constitutes an open menu and how
* selection works is defined within this class class as well as
* in the individual components transforming that state.
*/
public readonly openMenus: ReadonlyArray<IMenu>;
/**
* The menu that this instance operates on, taken from an
* electron Menu instance and converted into an IMenu model
* by menuFromElectronMenu.
*/
private readonly menu: IMenu;
/**
* A map between menu item ids and their corresponding MenuItem
*/
private readonly menuItemById: Map<string, MenuItem>;
/**
* Static constructor for the initial creation of an AppMenu instance
* from an IMenu instance.
*/
public static fromMenu(menu: IMenu): AppMenu {
const map = buildIdMap(menu);
const openMenus = [menu];
return new AppMenu(menu, openMenus, map);
}
// Used by static constructors and transformers.
private constructor(
menu: IMenu,
openMenus: ReadonlyArray<IMenu>,
menuItemById: Map<string, MenuItem>
) {
this.menu = menu;
this.openMenus = openMenus;
this.menuItemById = menuItemById;
}
/**
* Retrieves a menu item by its id.
*/
public getItemById(id: string): MenuItem | undefined {
return this.menuItemById.get(id);
}
/**
* Merges the current AppMenu state with a new menu while
* attempting to maintain selection state.
*/
public withMenu(newMenu: IMenu): AppMenu {
const newMap = buildIdMap(newMenu);
const newOpenMenus = new Array<IMenu>();
// Enumerate all currently open menus and attempt to recreate
// the openMenus array with the new menu instances
for (const openMenu of this.openMenus) {
let newOpenMenu: IMenu;
// No id means it's the root menu, simple enough.
if (!openMenu.id) {
newOpenMenu = newMenu;
} else {
// Menus share id with their parent item
const item = newMap.get(openMenu.id);
if (item && item.type === 'submenuItem') {
newOpenMenu = item.menu;
} else {
// This particular menu can't be found in the new menu
// structure, we have no choice but to bail here and
// not open this particular menu.
break;
}
}
let newSelectedItem: MenuItem | undefined;
if (openMenu.selectedItem) {
newSelectedItem = newMap.get(openMenu.selectedItem.id);
}
newOpenMenus.push({
id: newOpenMenu.id,
type: 'menu',
items: newOpenMenu.items,
selectedItem: newSelectedItem,
});
}
return new AppMenu(newMenu, newOpenMenus, newMap);
}
/**
* Creates a new copy of this AppMenu instance with the given submenu open.
*
* @param submenuItem - The item which submenu should be appended
* to the list of open menus.
*
* @param selectFirstItem - A convenience item for automatically selecting
* the first item in the newly opened menu.
*
* If false the new menu is opened without a selection.
*
* Defaults to false.
*/
public withOpenedMenu(
submenuItem: ISubmenuItem,
selectFirstItem = false
): AppMenu {
const ourMenuItem = this.menuItemById.get(submenuItem.id);
if (!ourMenuItem) {
return this;
}
if (ourMenuItem.type !== 'submenuItem') {
throw new Error(
`Attempt to open a submenu from an item of wrong type: ${
ourMenuItem.type
}`
);
}
const parentMenuIndex = this.openMenus.findIndex(
m => m.items.indexOf(ourMenuItem) !== -1
);
// The parent menu has apparently been closed in between, we could go and
// recreate it but it's probably not worth it.
if (parentMenuIndex === -1) {
return this;
}
const newOpenMenus = this.openMenus.slice(0, parentMenuIndex + 1);
if (selectFirstItem) {
// First selectable item.
const selectedItem = ourMenuItem.menu.items.find(itemIsSelectable);
newOpenMenus.push({ ...ourMenuItem.menu, selectedItem });
} else {
newOpenMenus.push(ourMenuItem.menu);
}
return new AppMenu(this.menu, newOpenMenus, this.menuItemById);
}
/**
* Creates a new copy of this AppMenu instance with the given menu removed from
* the list of open menus.
*
* @param menu - The menu which is to be closed, i.e. removed from the
* list of open menus.
*/
public withClosedMenu(menu: IMenu) {
// Root menu is always open and can't be closed
if (!menu.id) {
return this;
}
const ourMenuIndex = this.openMenus.findIndex(m => m.id === menu.id);
if (ourMenuIndex === -1) {
return this;
}
const newOpenMenus = this.openMenus.slice(0, ourMenuIndex);
return new AppMenu(this.menu, newOpenMenus, this.menuItemById);
}
/**
* Creates a new copy of this AppMenu instance with the list of open menus trimmed
* to not include any menus below the given menu.
*
* @param menu - The last menu which is to remain in the list of open
* menus, all menus below this level will be pruned from
* the list of open menus.
*/
public withLastMenu(menu: IMenu) {
const ourMenuIndex = this.openMenus.findIndex(m => m.id === menu.id);
if (ourMenuIndex === -1) {
return this;
}
const newOpenMenus = this.openMenus.slice(0, ourMenuIndex + 1);
return new AppMenu(this.menu, newOpenMenus, this.menuItemById);
}
/**
* Creates a new copy of this AppMenu instance in which the given menu item
* is selected.
*
* Additional semantics:
*
* All menus leading up to the given menu item will have their
* selection reset in such a fashion that the selection path
* points to the given menu item.
*
* All menus after the menu in which the given item resides
* will have their selections cleared.
*
* @param menuItem - The menu item which is to be selected.
*/
public withSelectedItem(menuItem: MenuItem) {
const ourMenuItem = this.menuItemById.get(menuItem.id);
// The item that someone is trying to select no longer
// exists, not much we can do about that.
if (!ourMenuItem) {
return this;
}
const parentMenuIndex = this.openMenus.findIndex(
m => m.items.indexOf(ourMenuItem) !== -1
);
// The menu which the selected item belongs to is no longer open,
// not much we can do about that.
if (parentMenuIndex === -1) {
return this;
}
const newOpenMenus = this.openMenus.slice();
const parentMenu = newOpenMenus[parentMenuIndex];
newOpenMenus[parentMenuIndex] = { ...parentMenu, selectedItem: ourMenuItem };
// All submenus below the active menu should have their selection cleared
for (let i = parentMenuIndex + 1; i < newOpenMenus.length; i++) {
newOpenMenus[i] = { ...newOpenMenus[i], selectedItem: undefined };
}
// Ensure that the path that lead us to the currently selected menu is
// selected. i.e. all menus above the currently active menu should have
// their selection reset to point to the currently active menu.
for (let i = parentMenuIndex - 1; i >= 0; i--) {
const menu = newOpenMenus[i];
const childMenu = newOpenMenus[i + 1];
const selectedItem = menu.items.find(
item => item.type === 'submenuItem' && item.id === childMenu.id
);
newOpenMenus[i] = { ...menu, selectedItem };
}
return new AppMenu(this.menu, newOpenMenus, this.menuItemById);
}
/**
* Creates a new copy of this AppMenu instance in which the given menu has had
* its selection state cleared.
*
* Additional semantics:
*
* All menus leading up to the given menu item will have their
* selection reset in such a fashion that the selection path
* points to the given menu.
*
* @param menu - The menu which is to have its selection state
* cleared.
*/
public withDeselectedMenu(menu: IMenu) {
const ourMenuIndex = this.openMenus.findIndex(m => m.id === menu.id);
// The menu that someone is trying to deselect is no longer open
// so no need to worry about selection
if (ourMenuIndex === -1) {
return this;
}
const ourMenu = this.openMenus[ourMenuIndex];
const newOpenMenus = this.openMenus.slice();
newOpenMenus[ourMenuIndex] = { ...ourMenu, selectedItem: undefined };
// Ensure that the path to the menu without an active selection is
// selected. i.e. all menus above should have their selection reset
// to point to the menu which no longer has an active selection.
for (let i = ourMenuIndex - 1; i >= 0; i--) {
const _menu = newOpenMenus[i];
const childMenu = newOpenMenus[i + 1];
const selectedItem = _menu.items.find(
item => item.type === 'submenuItem' && item.id === childMenu.id
);
newOpenMenus[i] = { ..._menu, selectedItem };
}
return new AppMenu(this.menu, newOpenMenus, this.menuItemById);
}
/**
* Creates a new copy of this AppMenu instance in which all state
* is reset. Resetting means that only the root menu is open and
* all selection state is cleared.
*/
public withReset() {
return new AppMenu(this.menu, [this.menu], this.menuItemById);
}
}

View File

@ -0,0 +1,135 @@
import { Tip } from './tip';
import { IMenu } from './app-menu';
import { WindowState } from './window-state';
import { PreferencesTab } from './preferences';
import { ApplicationTheme } from './application-theme';
/** All of the shared app state. */
export interface IAppState {
/**
* The current state of the window, ie maximized, minimized full-screen etc.
*/
readonly windowState: WindowState;
/**
* The current zoom factor of the window represented as a fractional number
* where 1 equals 100% (ie actual size) and 2 represents 200%.
*/
readonly windowZoomFactor: number;
/**
* A value indicating whether or not the current application
* window has focus.
*/
readonly appIsFocused: boolean;
readonly showWelcomeFlow: boolean;
readonly currentPopup: Popup | null;
readonly currentFoldout: Foldout | null;
/**
* A list of currently open menus with their selected items
* in the application menu.
*
* The semantics around what constitues an open menu and how
* selection works is defined by the AppMenu class and the
* individual components transforming that state.
*
* Note that as long as the renderer has received an application
* menu from the main process there will always be one menu
* "open", that is the root menu which can't be closed. In other
* words, a non-zero length appMenuState does not imply that the
* application menu should be visible. Currently thats defined by
* whether the app menu is open as a foldout (see currentFoldout).
*
* Not applicable on macOS unless the in-app application menu has
* been explicitly enabled for testing purposes.
*/
readonly appMenuState: ReadonlyArray<IMenu>;
readonly errors: ReadonlyArray<Error>;
/**
* The width of the repository sidebar.
*
* This affects the changes and history sidebar
* as well as the first toolbar section which contains
* repo selection on all platforms and repo selection and
* app menu on Windows.
*
* Lives on IAppState as opposed to IRepositoryState
* because it's used in the toolbar as well as the
* repository.
*/
readonly sidebarWidth: number;
/** The width of the commit summary column in the history view */
readonly commitSummaryWidth: number;
/** Whether we should hide the toolbar (and show inverted window controls) */
readonly titleBarStyle: 'light' | 'dark';
/**
* Used to highlight access keys throughout the app when the
* Alt key is pressed. Only applicable on non-macOS platforms.
*/
readonly highlightAccessKeys: boolean;
/** Whether we should show the update banner */
readonly isUpdateAvailableBannerVisible: boolean;
/** Whether we should show a confirmation dialog */
readonly askForConfirmationOnRepositoryRemoval: boolean;
/** Whether we should show a confirmation dialog */
readonly askForConfirmationOnDiscardChanges: boolean;
/** The currently selected appearance (aka theme) */
readonly selectedTheme: ApplicationTheme;
}
export enum PopupType {
Preferences = 1,
About,
Acknowledgements,
TermsAndConditions,
}
export type Popup =
| { type: PopupType.Preferences; initialSelectedTab?: PreferencesTab }
| { type: PopupType.About }
| { type: PopupType.Acknowledgements }
| { type: PopupType.TermsAndConditions }
;
export enum FoldoutType {
AppMenu,
AddMenu,
}
export interface AppMenuFoldout {
type: FoldoutType.AppMenu;
/**
* Whether or not the application menu was opened with the Alt key, this
* enables access key highlighting for applicable menu items as well as
* keyboard navigation by pressing access keys.
*/
enableAccessKeyNavigation: boolean;
/**
* Whether the menu was opened by pressing Alt (or Alt+X where X is an
* access key for one of the top level menu items). This is used as a
* one-time signal to the AppMenu to use some special semantics for
* selection and focus. Specifically it will ensure that the last opened
* menu will receive focus.
*/
openedWithAccessKey?: boolean;
}
export type Foldout =
| { type: FoldoutType.AddMenu }
| AppMenuFoldout
;

View File

@ -0,0 +1,54 @@
/**
* A set of the user-selectable appearances (aka themes)
*/
export enum ApplicationTheme {
Light,
Dark,
}
/**
* Gets the friendly name of an application theme for use
* in persisting to storage and/or calculating the required
* body class name to set in order to apply the theme.
*/
export function getThemeName(theme: ApplicationTheme): string {
switch (theme) {
case ApplicationTheme.Light:
return 'light';
case ApplicationTheme.Dark:
return 'dark';
default:
// return assertNever(theme, `Unknown theme ${theme}`);
}
}
// The key under which the currently selected theme is persisted
// in localStorage.
const applicationThemeKey = 'theme';
/**
* Load the currently selected theme from the persistent
* store (localStorage). If no theme is selected the default
* theme will be returned.
*/
export function getPersistedTheme(): ApplicationTheme {
return localStorage.getItem(applicationThemeKey) === 'dark'
? ApplicationTheme.Dark
: ApplicationTheme.Light;
}
/**
* Load the name of the currently selected theme from the persistent
* store (localStorage). If no theme is selected the default
* theme name will be returned.
*/
export function getPersistedThemeName(): string {
return getThemeName(getPersistedTheme());
}
/**
* Store the given theme in the persistent store (localStorage).
*/
export function setPersistedTheme(theme: ApplicationTheme) {
localStorage.setItem(applicationThemeKey, getThemeName(theme));
}

View File

@ -0,0 +1,174 @@
import { merge } from '@overflow/core/merge';
import { MenuIDs } from '../type';
import { IAppState } from './app-state';
import { TipState } from './tip';
import { AppMenu, MenuItem } from './app-menu';
export interface IMenuItemState {
readonly enabled?: boolean;
}
/**
* Utility class for coalescing updates to menu items
*/
class MenuStateBuilder {
private readonly _state: Map<MenuIDs, IMenuItemState>;
public constructor(state: Map<MenuIDs, IMenuItemState> = new Map()) {
this._state = state;
}
/**
* Returns an Map where each key is a MenuID and the values
* are IMenuItemState instances containing information about
* whether a particular menu item should be enabled/disabled or
* visible/hidden.
*/
public get state() {
return new Map<MenuIDs, IMenuItemState>(this._state);
}
private updateMenuItem<K extends keyof IMenuItemState>(
id: MenuIDs,
state: Pick<IMenuItemState, K>
) {
const currentState = this._state.get(id) || {};
this._state.set(id, merge(currentState, state));
}
/** Set the state of the given menu item id to enabled */
public enable(id: MenuIDs): this {
this.updateMenuItem(id, { enabled: true });
return this;
}
/** Set the state of the given menu item id to disabled */
public disable(id: MenuIDs): this {
this.updateMenuItem(id, { enabled: false });
return this;
}
/** Set the enabledness of the given menu item id */
public setEnabled(id: MenuIDs, enabled: boolean): this {
this.updateMenuItem(id, { enabled });
return this;
}
/**
* Create a new state builder by merging the current state with the state from
* the other state builder. This will replace values in `this` with values
* from `other`.
*/
public merge(other: MenuStateBuilder): MenuStateBuilder {
const merged = new Map<MenuIDs, IMenuItemState>(this._state);
for (const [key, value] of other._state) {
merged.set(key, value);
}
return new MenuStateBuilder(merged);
}
}
function menuItemStateEqual(state: IMenuItemState, menuItem: MenuItem) {
if (
state.enabled !== undefined &&
menuItem.type !== 'separator' &&
menuItem.enabled !== state.enabled
) {
return false;
}
return true;
}
const allMenuIds: ReadonlyArray<MenuIDs> = [
'save',
'save-as',
'preferences',
'open-in-shell',
'open-external-editor',
'about',
];
function getAllMenusDisabledBuilder(): MenuStateBuilder {
const menuStateBuilder = new MenuStateBuilder();
for (const menuId of allMenuIds) {
menuStateBuilder.disable(menuId);
}
return menuStateBuilder;
}
function getMenuState(state: IAppState): Map<MenuIDs, IMenuItemState> {
if (state.currentPopup) {
return getAllMenusDisabledBuilder().state;
}
return getAllMenusEnabledBuilder()
.merge(getInWelcomeFlowBuilder(state.showWelcomeFlow)).state;
}
function getAllMenusEnabledBuilder(): MenuStateBuilder {
const menuStateBuilder = new MenuStateBuilder();
for (const menuId of allMenuIds) {
menuStateBuilder.enable(menuId);
}
return menuStateBuilder;
}
function getInWelcomeFlowBuilder(inWelcomeFlow: boolean): MenuStateBuilder {
const welcomeScopedIds: ReadonlyArray<MenuIDs> = [
'preferences',
'about',
];
const menuStateBuilder = new MenuStateBuilder();
if (inWelcomeFlow) {
for (const id of welcomeScopedIds) {
menuStateBuilder.disable(id);
}
} else {
for (const id of welcomeScopedIds) {
menuStateBuilder.enable(id);
}
}
return menuStateBuilder;
}
/**
* Update the menu state in the main process.
*
* This function will set the enabledness and visibility of menu items
* in the main process based on the AppState. All changes will be
* batched together into one ipc message.
*/
export function updateMenuState(
state: IAppState,
currentAppMenu: AppMenu | null
) {
const menuState = getMenuState(state);
// Try to avoid updating sending the IPC message at all
// if we have a current app menu that we can compare against.
if (currentAppMenu) {
for (const [id, menuItemState] of menuState.entries()) {
const appMenuItem = currentAppMenu.getItemById(id);
if (appMenuItem && menuItemStateEqual(menuItemState, appMenuItem)) {
menuState.delete(id);
}
}
}
if (menuState.size === 0) {
return;
}
// because we can't send Map over the wire, we need to convert
// the remaining entries into an array that can be serialized
const array = new Array<{ id: MenuIDs; state: IMenuItemState }>();
menuState.forEach((value, key) => array.push({ id: key, state: value }));
// ipcUpdateMenuState(array);
}

View File

@ -0,0 +1,4 @@
export enum PreferencesTab {
Appearance = 0,
Advanced = 1,
}

37
src/commons/model/tip.ts Normal file
View File

@ -0,0 +1,37 @@
export enum TipState {
Unknown,
Unborn,
Detached,
Valid,
}
export interface IUnknownRepository {
readonly kind: TipState.Unknown;
}
export interface IUnbornRepository {
readonly kind: TipState.Unborn;
/**
* The symbolic reference that the unborn repository points to currently.
*
* Typically this will be "master" but a user can easily create orphaned
* branches externally.
*/
readonly ref: string;
}
export interface IDetachedHead {
readonly kind: TipState.Detached;
/**
* The commit identifier of the current tip of the repository.
*/
readonly currentSha: string;
}
export type Tip =
| IUnknownRepository
| IUnbornRepository
| IDetachedHead
;

View File

@ -17,10 +17,10 @@ export class ElectronProxyService {
private store: Store<any>, private store: Store<any>,
private launchService: LaunchService, private launchService: LaunchService,
) { ) {
this.bindIPCRenderer(); this.bindIpcEventHandlers();
} }
private bindIPCRenderer(): void { private bindIpcEventHandlers(): void {
ipcRenderer.on('menu-event', ipcRenderer.on('menu-event',
(event: Electron.IpcMessageEvent, { name }: { name: MenuEvent }) => { (event: Electron.IpcMessageEvent, { name }: { name: MenuEvent }) => {
@ -29,9 +29,9 @@ export class ElectronProxyService {
ipcRenderer.on('launch-timing-stats', ipcRenderer.on('launch-timing-stats',
(event: Electron.IpcMessageEvent, { state }: { state: LaunchState }) => { (event: Electron.IpcMessageEvent, { state }: { state: LaunchState }) => {
console.info(`App ready time: ${state.mainReadyTime}ms`) console.info(`App ready time: ${state.mainReadyTime}ms`);
console.info(`Load time: ${state.loadTime}ms`) console.info(`Load time: ${state.loadTime}ms`);
console.info(`Renderer ready time: ${state.rendererReadyTime}ms`) console.info(`Renderer ready time: ${state.rendererReadyTime}ms`);
this.launchService.save(state).pipe( this.launchService.save(state).pipe(
map((id: number) => { map((id: number) => {
@ -42,11 +42,15 @@ export class ElectronProxyService {
take(1), take(1),
).subscribe(); ).subscribe();
} }
) );
} }
public sendReady(time: number): void { public sendReady(time: number): void {
ipcRenderer.send('renderer-ready', time); ipcRenderer.send('renderer-ready', time);
} }
public getAppMenu(): void {
ipcRenderer.send('get-app-menu');
}
} }

View File

@ -1,4 +1,7 @@
export type MenuEvent = export type MenuEvent =
| 'save'
| 'save-as'
| 'show-preferences'
| 'show-about' | 'show-about'
| 'open-external-editor' | 'open-external-editor'
| 'select-all'; | 'select-all';

View File

@ -1,4 +1,6 @@
export type MenuIDs = export type MenuIDs =
| 'save'
| 'save-as'
| 'preferences' | 'preferences'
| 'open-in-shell' | 'open-in-shell'
| 'open-external-editor' | 'open-external-editor'

View File

@ -7,12 +7,13 @@ import { Subscription, PartialObserver } from 'rxjs';
import { Emitter } from '@overflow/core/emitter'; import { Emitter } from '@overflow/core/emitter';
import { encodePathAsUrl } from '@overflow/core/path'; import { encodePathAsUrl } from '@overflow/core/path';
import { registerWindowStateChangedEvents } from '@overflow/core/window-state'; import { registerWindowStateChangedEvents } from '../commons/model/window-state';
import { URLActionType } from '@overflow/core/parse-app-url'; import { URLActionType } from '@overflow/core/parse-app-url';
import { now } from '@overflow/core/now'; import { now } from '@overflow/core/now';
import { MenuEvent } from '../commons/type'; import { MenuEvent } from '../commons/type';
import { LaunchState } from '../commons/model'; import { LaunchState } from '../commons/model';
import { menuFromElectronMenu } from '../commons/model/app-menu';
let windowStateKeeper: any | null = null; let windowStateKeeper: any | null = null;
@ -223,13 +224,13 @@ export class AppWindow {
} }
/** Send the app menu to the renderer. */ /** Send the app menu to the renderer. */
// public sendAppMenu() { public sendAppMenu() {
// const appMenu = Menu.getApplicationMenu(); const appMenu = Menu.getApplicationMenu();
// if (appMenu) { if (appMenu) {
// const menu = menuFromElectronMenu(appMenu); const menu = menuFromElectronMenu(appMenu);
// this.window.webContents.send('app-menu', { menu }); this.window.webContents.send('app-menu', { menu });
// } }
// } }
/** Send a certificate error to the renderer. */ /** Send a certificate error to the renderer. */
public sendCertificateError( public sendCertificateError(

View File

@ -15,6 +15,10 @@ import { AppWindow } from './app-window';
import { handleSquirrelEvent } from './squirrel-updater'; import { handleSquirrelEvent } from './squirrel-updater';
import { openDirectorySafe } from './shell'; import { openDirectorySafe } from './shell';
import { buildDefaultMenu } from './menu/build-default';
import { MenuEvent } from '../commons/type';
import { findMenuItemByID } from './menu/find-menu-item';
import { IMenuItemState } from '../commons/model/menu-update';
enableSourceMaps(); enableSourceMaps();
@ -193,83 +197,81 @@ app.on('ready', () => {
createWindow(); createWindow();
// let menu = buildDefaultMenu(); let menu = buildDefaultMenu();
// Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
// ipcMain.on( ipcMain.on(
// 'update-preferred-app-menu-item-labels', 'update-preferred-app-menu-item-labels',
// ( (
// event: Electron.IpcMessageEvent, event: Electron.IpcMessageEvent,
// labels: { editor?: string; pullRequestLabel?: string; shell: string } labels: { editor?: string; pullRequestLabel?: string; shell: string }
// ) => { ) => {
// menu = buildDefaultMenu( menu = buildDefaultMenu(
// labels.editor, labels.shell,
// labels.shell, );
// labels.pullRequestLabel Menu.setApplicationMenu(menu);
// ); if (mainWindow) {
// Menu.setApplicationMenu(menu); mainWindow.sendAppMenu();
// if (mainWindow) { }
// mainWindow.sendAppMenu(); }
// } );
// }
// );
// ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => { ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => {
// const { name }: { name: MenuEvent } = event as any; const { name }: { name: MenuEvent } = event as any;
// if (mainWindow) { if (mainWindow) {
// mainWindow.sendMenuEvent(name); mainWindow.sendMenuEvent(name);
// } }
// }); });
/** /**
* An event sent by the renderer asking that the menu item with the given id * An event sent by the renderer asking that the menu item with the given id
* is executed (ie clicked). * is executed (ie clicked).
*/ */
// ipcMain.on( ipcMain.on(
// 'execute-menu-item', 'execute-menu-item',
// (event: Electron.IpcMessageEvent, { id }: { id: string }) => { (event: Electron.IpcMessageEvent, { id }: { id: string }) => {
// const menuItem = findMenuItemByID(menu, id); const menuItem = findMenuItemByID(menu, id);
// if (menuItem) { if (menuItem) {
// const window = BrowserWindow.fromWebContents(event.sender); const window = BrowserWindow.fromWebContents(event.sender);
// const fakeEvent = { preventDefault: () => { }, sender: event.sender }; const fakeEvent = { preventDefault: () => { }, sender: event.sender };
// menuItem.click(fakeEvent, window, event.sender); menuItem.click(fakeEvent, window, event.sender);
// } }
// } }
// ); );
// ipcMain.on( ipcMain.on(
// 'update-menu-state', 'update-menu-state',
// ( (
// event: Electron.IpcMessageEvent, event: Electron.IpcMessageEvent,
// items: Array<{ id: string; state: IMenuItemState }> items: Array<{ id: string; state: IMenuItemState }>
// ) => { ) => {
// let sendMenuChangedEvent = false; let sendMenuChangedEvent = false;
// for (const item of items) { for (const item of items) {
// const { id, state } = item; const { id, state } = item;
// const menuItem = findMenuItemByID(menu, id); const menuItem = findMenuItemByID(menu, id);
// if (menuItem) { if (menuItem) {
// // Only send the updated app menu when the state actually changes // Only send the updated app menu when the state actually changes
// // or we might end up introducing a never ending loop between // or we might end up introducing a never ending loop between
// // the renderer and the main process // the renderer and the main process
// if ( if (
// state.enabled !== undefined && state.enabled !== undefined &&
// menuItem.enabled !== state.enabled menuItem.enabled !== state.enabled
// ) { ) {
// menuItem.enabled = state.enabled; menuItem.enabled = state.enabled;
// sendMenuChangedEvent = true; sendMenuChangedEvent = true;
// } }
// } else { } else {
// fatalError(`Unknown menu id: ${id}`); // fatalError(`Unknown menu id: ${id}`);
// } }
// } }
// if (sendMenuChangedEvent && mainWindow) { if (sendMenuChangedEvent && mainWindow) {
// mainWindow.sendAppMenu(); mainWindow.sendAppMenu();
// } }
// } }
// ); );
// ipcMain.on( // ipcMain.on(
// 'show-contextual-menu', // 'show-contextual-menu',
@ -287,11 +289,11 @@ app.on('ready', () => {
* An event sent by the renderer asking for a copy of the current * An event sent by the renderer asking for a copy of the current
* application menu. * application menu.
*/ */
// ipcMain.on('get-app-menu', () => { ipcMain.on('get-app-menu', () => {
// if (mainWindow) { if (mainWindow) {
// mainWindow.sendAppMenu(); mainWindow.sendAppMenu();
// } }
// }); });
// ipcMain.on( // ipcMain.on(
// 'show-certificate-trust-dialog', // 'show-certificate-trust-dialog',

View File

@ -0,0 +1,310 @@
import { Menu, ipcMain, shell, app } from 'electron';
import { ensureDir } from 'fs-extra';
import { ensureItemIds } from './ensure-item-ids';
import { MenuEvent } from '../../commons/type/menu-event';
import { openDirectorySafe } from '../shell';
const defaultShellLabel = __DARWIN__
? 'Open in Terminal'
: 'Open in Command Prompt';
export function buildDefaultMenu(
shellLabel: string = defaultShellLabel,
): Electron.Menu {
const template = new Array<Electron.MenuItemConstructorOptions>();
const separator: Electron.MenuItemConstructorOptions = { type: 'separator' };
if (__DARWIN__) {
template.push({
label: 'overFlow Scanner',
submenu: [
{
label: 'About overFlow Scanner',
click: emit('show-about'),
id: 'about',
},
separator,
{
label: 'Preferences…',
id: 'preferences',
accelerator: 'CmdOrCtrl+,',
click: emit('show-preferences'),
},
separator,
{
role: 'services',
submenu: [],
},
separator,
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
separator,
{ role: 'quit' },
],
});
}
const fileMenu: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'File' : '&File',
submenu: [
{
label: __DARWIN__ ? 'Save…' : 'Save…',
id: 'new-repository',
click: emit('save'),
accelerator: 'CmdOrCtrl+S',
},
{
label: __DARWIN__ ? 'Save as…' : 'Save as…',
id: 'add-local-repository',
accelerator: 'CmdOrCtrl+Shift+S',
click: emit('save-as'),
}
],
};
if (!__DARWIN__) {
const fileItems = fileMenu.submenu as Electron.MenuItemConstructorOptions[];
fileItems.push(
separator,
{
label: '&Options…',
id: 'preferences',
accelerator: 'CmdOrCtrl+,',
click: emit('show-preferences'),
},
separator,
{ role: 'quit' }
);
}
template.push(fileMenu);
template.push({
label: __DARWIN__ ? 'Edit' : '&Edit',
submenu: [
{ role: 'undo', label: __DARWIN__ ? 'Undo' : '&Undo' },
{ role: 'redo', label: __DARWIN__ ? 'Redo' : '&Redo' },
separator,
{ role: 'cut', label: __DARWIN__ ? 'Cut' : 'Cu&t' },
{ role: 'copy', label: __DARWIN__ ? 'Copy' : '&Copy' },
{ role: 'paste', label: __DARWIN__ ? 'Paste' : '&Paste' },
{
label: __DARWIN__ ? 'Select All' : 'Select &all',
accelerator: 'CmdOrCtrl+A',
click: emit('select-all'),
},
],
});
template.push({
label: __DARWIN__ ? 'View' : '&View',
submenu: [
{
label: __DARWIN__ ? 'Toggle Full Screen' : 'Toggle &full screen',
role: 'togglefullscreen',
},
separator,
{
label: __DARWIN__ ? 'Reset Zoom' : 'Reset zoom',
accelerator: 'CmdOrCtrl+0',
click: zoom(ZoomDirection.Reset),
},
{
label: __DARWIN__ ? 'Zoom In' : 'Zoom in',
accelerator: 'CmdOrCtrl+=',
click: zoom(ZoomDirection.In),
},
{
label: __DARWIN__ ? 'Zoom Out' : 'Zoom out',
accelerator: 'CmdOrCtrl+-',
click: zoom(ZoomDirection.Out),
},
separator,
{
label: '&Reload',
id: 'reload-window',
// Ctrl+Alt is interpreted as AltGr on international keyboards and this
// can clash with other shortcuts. We should always use Ctrl+Shift for
// chorded shortcuts, but this menu item is not a user-facing feature
// so we are going to keep this one around and save Ctrl+Shift+R for
// a different shortcut in the future...
accelerator: 'CmdOrCtrl+Alt+R',
click(item: any, focusedWindow: Electron.BrowserWindow) {
if (focusedWindow) {
focusedWindow.reload();
}
},
visible: __RELEASE_CHANNEL__ === 'development',
},
{
id: 'show-devtools',
label: __DARWIN__
? 'Toggle Developer Tools'
: '&Toggle developer tools',
accelerator: (() => {
return __DARWIN__ ? 'Alt+Command+I' : 'Ctrl+Shift+I';
})(),
click(item: any, focusedWindow: Electron.BrowserWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
},
},
],
});
if (__DARWIN__) {
template.push({
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
{ role: 'close' },
separator,
{ role: 'front' },
],
});
}
const submitIssueItem: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Report Issue…' : 'Report issue…',
click() {
shell.openExternal('https://github.com/desktop/desktop/issues/new/choose');
},
};
const contactSupportItem: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Contact overFlow Scanner Support…' : '&Contact overFlow Scanner support…',
click() {
shell.openExternal(
`https://github.com/contact?from_desktop_app=1&app_version=${app.getVersion()}`
);
},
};
const showUserGuides: Electron.MenuItemConstructorOptions = {
label: 'Show User Guides',
click() {
shell.openExternal('https://help.github.com/desktop/guides/');
},
};
const helpItems = [
submitIssueItem,
contactSupportItem,
showUserGuides,
];
if (__DARWIN__) {
template.push({
role: 'help',
submenu: helpItems,
});
} else {
template.push({
label: '&Help',
submenu: [
...helpItems,
separator,
{
label: '&About overFlow Scanner',
click: emit('show-about'),
id: 'about',
},
],
});
}
ensureItemIds(template);
return Menu.buildFromTemplate(template);
}
type ClickHandler = (
menuItem: Electron.MenuItem,
browserWindow: Electron.BrowserWindow,
event: Electron.Event
) => void;
/**
* Utility function returning a Click event handler which, when invoked, emits
* the provided menu event over IPC.
*/
function emit(name: MenuEvent): ClickHandler {
return (menuItem, window) => {
if (window) {
window.webContents.send('menu-event', { name });
} else {
ipcMain.emit('menu-event', { name });
}
};
}
enum ZoomDirection {
Reset,
In,
Out,
}
/** The zoom steps that we support, these factors must sorted */
const ZoomInFactors = [1, 1.1, 1.25, 1.5, 1.75, 2];
const ZoomOutFactors = ZoomInFactors.slice().reverse();
/**
* Returns the element in the array that's closest to the value parameter. Note
* that this function will throw if passed an empty array.
*/
function findClosestValue(arr: Array<number>, value: number) {
return arr.reduce((previous, current) => {
return Math.abs(current - value) < Math.abs(previous - value)
? current
: previous;
});
}
/**
* Figure out the next zoom level for the given direction and alert the renderer
* about a change in zoom factor if necessary.
*/
function zoom(direction: ZoomDirection): ClickHandler {
return (menuItem, window) => {
if (!window) {
return;
}
const { webContents } = window;
if (direction === ZoomDirection.Reset) {
webContents.setZoomFactor(1);
webContents.send('zoom-factor-changed', 1);
} else {
webContents.getZoomFactor(rawZoom => {
const zoomFactors =
direction === ZoomDirection.In ? ZoomInFactors : ZoomOutFactors;
// So the values that we get from getZoomFactor are floating point
// precision numbers from chromium that don't always round nicely so
// we'll have to do a little trick to figure out which of our supported
// zoom factors the value is referring to.
const currentZoom = findClosestValue(zoomFactors, rawZoom);
const nextZoomLevel = zoomFactors.find(
f =>
direction === ZoomDirection.In ? f > currentZoom : f < currentZoom
);
// If we couldn't find a zoom level (likely due to manual manipulation
// of the zoom factor in devtools) we'll just snap to the closest valid
// factor we've got.
const newZoom =
nextZoomLevel === undefined ? currentZoom : nextZoomLevel;
webContents.setZoomFactor(newZoom);
webContents.send('zoom-factor-changed', newZoom);
});
}
};
}

View File

@ -0,0 +1,44 @@
function getItemId(template: Electron.MenuItemConstructorOptions) {
return template.id || template.label || template.role || 'unknown';
}
/**
* Ensures that all menu items in the given template are assigned an id
* by recursively traversing the template and mutating items in place.
*
* Items which already have an id are left alone, the other get a unique,
* but consistent id based on their label or role and their position in
* the menu hierarchy.
*
* Note that this does not do anything to prevent the case where items have
* explicitly been given duplicate ids.
*/
export function ensureItemIds(
template: ReadonlyArray<Electron.MenuItemConstructorOptions>,
prefix = '@',
seenIds = new Set<string>()
) {
for (const item of template) {
let counter = 0;
let id = item.id;
// Automatically generate an id if one hasn't been explicitly provided
if (!id) {
// Ensure that multiple items with the same key gets suffixed with a number
// i.e. @.separator, @.separator1 @.separator2 etc
do {
id = `${prefix}.${getItemId(item)}${counter++ || ''}`;
} while (seenIds.has(id));
}
item.id = id;
seenIds.add(id);
if (item.submenu) {
const subMenuTemplate = item.submenu as ReadonlyArray<
Electron.MenuItemConstructorOptions
>;
ensureItemIds(subMenuTemplate, item.id, seenIds);
}
}
}

View File

@ -0,0 +1,24 @@
/** Find the menu item with the given ID. */
export function findMenuItemByID(
menu: Electron.Menu,
id: string
): Electron.MenuItem | null {
const items = menu.items;
for (const item of items) {
// The electron type definition doesn't include the `id` field :(
if ((item as any).id === id) {
return item;
}
// We're assuming we're working with an already created menu.
const submenu = item.submenu as Electron.Menu;
if (submenu) {
const found = findMenuItemByID(submenu, id);
if (found) {
return found;
}
}
}
return null;
}

View File

@ -143,7 +143,7 @@ async function updateShortcut(): Promise<void> {
const desktopShortcutPath = Path.join( const desktopShortcutPath = Path.join(
homeDirectory, homeDirectory,
'Desktop', 'Desktop',
'GitHub Desktop.lnk' 'overFlow Scanner.lnk'
); );
const exists = await pathExists(desktopShortcutPath); const exists = await pathExists(desktopShortcutPath);
const locations: ShortcutLocations = exists const locations: ShortcutLocations = exists

View File

@ -1,8 +1,7 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../out-tsc/app", "outDir": "../out-tsc/app"
"types": ["node"]
}, },
"exclude": [ "exclude": [
"src/test.ts", "src/test.ts",

View File

@ -13,6 +13,9 @@
"typeRoots": [ "typeRoots": [
"node_modules/@types" "node_modules/@types"
], ],
"types": [
"node"
],
"lib": [ "lib": [
"es2015", "es2015",
"es2017", "es2017",

View File

@ -46,7 +46,7 @@
"no-arg": true, "no-arg": true,
"no-bitwise": true, "no-bitwise": true,
"no-console": [ "no-console": [
true, false,
"debug", "debug",
"info", "info",
"time", "time",

View File

@ -243,6 +243,12 @@
semver "^5.3.0" semver "^5.3.0"
semver-intersect "^1.1.2" semver-intersect "^1.1.2"
"@types/fs-extra@^5.0.4":
version "5.0.4"
resolved "https://nexus.loafle.net/repository/npm-all/@types/fs-extra/-/fs-extra-5.0.4.tgz#b971134d162cc0497d221adde3dbb67502225599"
dependencies:
"@types/node" "*"
"@types/jasmine@*", "@types/jasmine@~2.8.6": "@types/jasmine@*", "@types/jasmine@~2.8.6":
version "2.8.8" version "2.8.8"
resolved "https://nexus.loafle.net/repository/npm-all/@types/jasmine/-/jasmine-2.8.8.tgz#bf53a7d193ea8b03867a38bfdb4fbb0e0bf066c9" resolved "https://nexus.loafle.net/repository/npm-all/@types/jasmine/-/jasmine-2.8.8.tgz#bf53a7d193ea8b03867a38bfdb4fbb0e0bf066c9"
@ -257,6 +263,10 @@
version "4.14.116" version "4.14.116"
resolved "https://nexus.loafle.net/repository/npm-all/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9" resolved "https://nexus.loafle.net/repository/npm-all/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
"@types/node@*":
version "10.7.1"
resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
"@types/node@^6.0.46": "@types/node@^6.0.46":
version "6.0.116" version "6.0.116"
resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-6.0.116.tgz#2f9cd62b4ecc4927e3942e2655c182eecf5b45f1" resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-6.0.116.tgz#2f9cd62b4ecc4927e3942e2655c182eecf5b45f1"
@ -265,9 +275,9 @@
version "8.10.25" version "8.10.25"
resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-8.10.25.tgz#801fe4e39372cef18f268db880a5fbfcf71adc7e" resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-8.10.25.tgz#801fe4e39372cef18f268db880a5fbfcf71adc7e"
"@types/node@~8.9.4": "@types/node@^8.10.4":
version "8.9.5" version "8.10.26"
resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-8.9.5.tgz#162b864bc70be077e6db212b322754917929e976" resolved "https://nexus.loafle.net/repository/npm-all/@types/node/-/node-8.10.26.tgz#950e3d4e6b316ba6e1ae4e84d9155aba67f88c2f"
"@types/q@^0.0.32": "@types/q@^0.0.32":
version "0.0.32" version "0.0.32"