Compare commits

..
12 Commits
250 changed files with 15580 additions and 25382 deletions
+210
View File
@@ -0,0 +1,210 @@
step-restore-cache: &step-restore-cache
restore_cache:
keys:
- v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
- v1-modules-{{ arch }}
step-install: &step-install
run: npx yarn --frozen-lockfile && rm -rf node_modules/electron/dist
step-save-cache: &step-save-cache
save_cache:
paths:
- node_modules
key: v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
step-install-electron: &step-install-electron
run: cd node_modules/electron && node install.js
steps-test: &steps-test
steps:
- checkout
- *step-restore-cache
- *step-install
- *step-save-cache
- *step-install-electron
- run: yarn lint
- run: yarn test --maxWorkers=4
steps-build: &steps-build
steps:
- checkout
- *step-restore-cache
- *step-install
- *step-save-cache
- *step-install-electron
- when:
condition: <<parameters.on_linux>>
steps:
- run:
name: Install Linux Dependencies
command: sudo apt install -y rpm fakeroot
- when:
condition: <<parameters.on_mac>>
steps:
- run:
name: Import macOS Certificate
command: .circleci/import-macos-cert.sh
- when:
condition: <<parameters.on_win>>
steps:
- run:
name: Import Windows Certificate
command: |
if (Test-Path Env:\WINDOWS_CERTIFICATE_P12) {
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$filename = "$workingDirectory\cert.p12"
$bytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_P12)
[IO.File]::WriteAllBytes($filename, $bytes)
}
shell: powershell.exe
- run:
name: Publish dry run
command: yarn run publish --dry-run --arch=$TARGET_ARCH
- store_artifacts:
path: out/make
destination: dist
- persist_to_workspace:
root: .
paths:
- out/make
- out/publish-dry-run
version: 2.1
orbs:
win: circleci/windows@1.0.0
jobs:
test-linux:
docker:
- image: circleci/node:10
<<: *steps-test
build-linux:
docker:
- image: circleci/node:10-buster
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: true
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: false
<<: *steps-build
test-mac:
macos:
xcode: "10.2.0"
<<: *steps-test
build-mac:
macos:
xcode: "10.2.0"
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: true
on_win:
type: boolean
default: false
<<: *steps-build
test-windows:
executor:
name: win/vs2019
shell: bash.exe
<<: *steps-test
build-windows-ia32:
executor:
name: win/vs2019
shell: bash.exe
environment:
TARGET_ARCH: ia32
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: true
<<: *steps-build
build-windows-x64:
executor:
name: win/vs2019
shell: bash.exe
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: true
<<: *steps-build
finalize:
docker:
- image: circleci/node:10
steps:
- checkout
- *step-restore-cache
- *step-install
- attach_workspace:
at: .
- store_artifacts:
path: out/make
destination: dist
release:
docker:
- image: circleci/node:10
steps:
- checkout
- *step-restore-cache
- *step-install
- attach_workspace:
at: .
- run: yarn publish --from-dry-run
workflows:
version: 2
build:
jobs:
- test-linux
- test-mac
- test-windows
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
- finalize:
requires:
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
- release:
requires:
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
filters:
branches:
ignore: /.*/
tags:
only: /v.*/
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
set -e
if [[ ! -z $MACOS_CERT_P12 ]]
then
export CERTIFICATE_P12=cert.p12;
echo $MACOS_CERT_P12 | base64 --decode > $CERTIFICATE_P12;
export KEYCHAIN=build.keychain;
# Create the keychain with a password
security create-keychain -p travis $KEYCHAIN;
# Make the custom keychain default, so xcodebuild will use it for signing
security default-keychain -s $KEYCHAIN;
# Unlock the keychain
security unlock-keychain -p travis $KEYCHAIN;
# Add certificates to keychain and allow codesign to access them
# Apple Worldwide Developer Relations Certification Authority
security import ./tools/certs/apple.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer Authentication Certification Authority
security import ./tools/certs/dac.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer ID Felix
security import $CERTIFICATE_P12 -k $KEYCHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign 2>&1 >/dev/null;
rm $CERTIFICATE_P12;
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEYCHAIN
# Echo the identity
security find-identity -v -p codesigning
fi
-4
View File
@@ -1,4 +0,0 @@
/out
/dist
/coverage
/static
-30
View File
@@ -1,30 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
extends: [
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
};
-1
View File
@@ -21,7 +21,6 @@
/out/
node_modules/
SHASUMS256.txt
**/yarn.lock
compile_commands.json
.envrc
.cache
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
trailingComma: "all",
singleQuote: true,
};
-63
View File
@@ -1,63 +0,0 @@
language: node_js
node_js: "12"
os:
- linux
- osx
dist: bionic
osx_image: xcode10
cache:
npm: true
directories:
- $HOME/.cache/electron
addons:
apt:
packages:
- fakeroot
- rpm
branches:
only:
- master
- /^v\d+\.\d+\.\d+/
install:
- npm ci
- |
if [[ "$TRAVIS_OS_NAME" == "osx" && "$TRAVIS_SECURE_ENV_VARS" == "true" ]]; then
export CERTIFICATE_P12=cert.p12;
echo $MACOS_CERT_P12 | base64 --decode > $CERTIFICATE_P12;
export KEYCHAIN=build.keychain;
# Create the keychain with a password
security create-keychain -p travis $KEYCHAIN;
# Make the custom keychain default, so xcodebuild will use it for signing
security default-keychain -s $KEYCHAIN;
# Unlock the keychain
security unlock-keychain -p travis $KEYCHAIN;
# Add certificates to keychain and allow codesign to access them
# Apple Worldwide Developer Relations Certification Authority
security import ./tools/certs/apple.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer Authentication Certification Authority
security import ./tools/certs/dac.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer ID Felix
security import $CERTIFICATE_P12 -k $KEYCHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign 2>&1 >/dev/null;
rm $CERTIFICATE_P12;
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEYCHAIN
# Echo the identity
security find-identity -v -p codesigning
fi
script:
- npm run lint
- npm run test:ci
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run test:coverage; fi
- if test -z "$TRAVIS_TAG"; then npm run make; fi
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
+1 -3
View File
@@ -1,8 +1,6 @@
# <img src="https://user-images.githubusercontent.com/378023/49785546-4b7f7000-fd64-11e8-8033-a52c73a07fbf.png" width="60px" align="center" alt="Electron Fiddle icon"> Electron Fiddle
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle)
[![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](https://coveralls.io/github/electron/fiddle?branch=master)
[![Electron Discord Invite](https://img.shields.io/discord/745037351163527189?color=%237289DA&label=chat&logo=discord&logoColor=white)](https://discord.com/invite/electron)
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle) [![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](https://coveralls.io/github/electron/fiddle?branch=master)
Electron Fiddle lets you create and play with small Electron experiments. It
greets you with a quick-start template after opening change a few things,
-37
View File
@@ -1,37 +0,0 @@
environment:
matrix:
- nodejs_version: "12"
init:
- git config --global core.symlinks true
install:
# Setup the code signing certificate
- ps: >-
if (Test-Path Env:\WINDOWS_CERTIFICATE_P12) {
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$filename = "$workingDirectory\cert.p12"
$bytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_P12)
[IO.File]::WriteAllBytes($filename, $bytes)
}
- ps: Install-Product node $env:nodejs_version x64
- node --version
- npm ci
cache:
- '%APPDATA%\npm-cache -> appveyor.yml'
test_script:
- node --version
- npm --version
- npm run lint
- npm run test
artifacts:
- path: 'out\make\squirrel.windows\**\*.exe'
build_script:
- if %APPVEYOR_REPO_TAG% EQU false npm run make
- if %APPVEYOR_REPO_TAG% EQU true npm run publish
- if %APPVEYOR_REPO_TAG% EQU true npm run publish -- --arch=ia32
- ps: Tree ./out/make /F
+39 -46
View File
@@ -1,13 +1,15 @@
const path = require('path');
const fs = require('fs');
const packageJson = require('./package.json');
/* tslint:disable */
const { version } = packageJson;
const iconDir = path.resolve(__dirname, 'assets', 'icons');
const path = require('path')
const fs = require('fs')
const packageJson = require('./package.json')
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
const config = {
hooks: {
generateAssets: require('./tools/generateAssets'),
generateAssets: require('./tools/generateAssets')
},
packagerConfig: {
name: 'Electron Fiddle',
@@ -16,18 +18,14 @@ const config = {
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
appBundleId: 'com.electron.fiddle',
usageDescription: {
Camera:
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
Microphone:
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone',
Camera: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
Microphone: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone'
},
appCategoryType: 'public.app-category.developer-tools',
protocols: [
{
name: 'Electron Fiddle Launch Protocol',
schemes: ['electron-fiddle'],
},
],
protocols: [{
name: 'Electron Fiddle Launch Protocol',
schemes: ['electron-fiddle']
}],
win32metadata: {
CompanyName: 'Electron Community',
OriginalFilename: 'Electron Fiddle',
@@ -36,10 +34,10 @@ const config = {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
entitlements: 'static/entitlements.plist',
'entitlements': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library',
},
'signature-flags': 'library'
}
},
makers: [
{
@@ -51,44 +49,41 @@ const config = {
: process.env.WINDOWS_CERTIFICATE_FILE;
if (!certificateFile || !fs.existsSync(certificateFile)) {
console.warn(
`Warning: Could not find certificate file at ${certificateFile}`,
);
console.warn(`Warning: Could not find certificate file at ${certificateFile}`)
}
return {
name: 'electron-fiddle',
authors: 'Electron Community',
exe: 'electron-fiddle.exe',
iconUrl:
'https://raw.githubusercontent.com/electron/fiddle/0119f0ce697f5ff7dec4fe51f17620c78cfd488b/assets/icons/fiddle.ico',
iconUrl: 'https://raw.githubusercontent.com/electron/fiddle/0119f0ce697f5ff7dec4fe51f17620c78cfd488b/assets/icons/fiddle.ico',
loadingGif: './assets/loading.gif',
noMsi: true,
remoteReleases: '',
setupExe: `electron-fiddle-${version}-${arch}-setup.exe`,
setupIcon: path.resolve(iconDir, 'fiddle.ico'),
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
certificateFile,
};
},
certificateFile
}
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
platforms: ['darwin']
},
{
name: '@electron-forge/maker-deb',
platforms: ['linux'],
config: {
icon: {
scalable: path.resolve(iconDir, 'fiddle.svg'),
},
},
scalable: path.resolve(iconDir, 'fiddle.svg')
}
}
},
{
name: '@electron-forge/maker-rpm',
platforms: ['linux'],
},
platforms: ['linux']
}
],
publishers: [
{
@@ -96,14 +91,14 @@ const config = {
config: {
repository: {
owner: 'electron',
name: 'fiddle',
name: 'fiddle'
},
draft: true,
prerelease: false,
},
},
],
};
prerelease: false
}
}
]
}
function notarizeMaybe() {
if (process.platform !== 'darwin') {
@@ -126,9 +121,7 @@ function notarizeMaybe() {
}
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn(
'Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!',
);
console.warn('Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!');
return;
}
@@ -136,11 +129,11 @@ function notarizeMaybe() {
appBundleId: 'com.electron.fiddle',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ',
};
ascProvider: 'LT94ZKYDCJ'
}
}
notarizeMaybe();
notarizeMaybe()
// Finally, export it
module.exports = config;
module.exports = config
-20287
View File
File diff suppressed because it is too large Load Diff
+13 -34
View File
@@ -1,7 +1,7 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.16.0",
"version": "0.15.1",
"description": "The easiest way to get started with Electron",
"repository": "https://github.com/electron/fiddle",
"main": "./dist/src/main/main",
@@ -9,8 +9,9 @@
"contributors": "node ./tools/contributors.js",
"less": "node ./tools/lessc.js",
"lint:style": "stylelint \"./src/less/*.less\" --fix",
"lint:ts": "eslint \"./**/*.{js,ts,tsx}\" --fix",
"lint:templates": "standard \"./static/show-me/**/*.js\" --fix",
"lint:ts": "tslint -c tslint.json -p tsconfig.json -e \"node_modules/**/*.ts\" --fix",
"lint:tests": "tslint \"./tests/**/*.ts{,x}\" -c tslint.json --fix",
"lint:templates": "standard \"./static/show-me/**/*.js\"",
"lint": "npm-run-all \"lint:*\"",
"make": "electron-forge make",
"package": "electron-forge package",
@@ -42,8 +43,8 @@
"@octokit/rest": "^16.43.1",
"@sentry/electron": "^1.2.1",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.2",
"electron-devtools-installer": "^3.1.1",
"electron-default-menu": "^1.0.1",
"electron-devtools-installer": "^2.2.4",
"electron-squirrel-startup": "^1.0.0",
"extract-zip": "^1.6.7",
"fix-path": "^3.0.0",
@@ -59,7 +60,7 @@
"semver": "^7.1.3",
"tmp": "0.1.0",
"tslib": "^1.11.1",
"update-electron-app": "^2.0.1"
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@babel/core": "^7.8.6",
@@ -80,51 +81,29 @@
"@types/react-dom": "^16.9.5",
"@types/semver": "^7.1.0",
"@types/tmp": "0.1.0",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"chokidar": "^3.3.1",
"coveralls": "^3.0.9",
"electron": "^9.3.1",
"electron": "8.0.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"fetch-mock-jest": "^1.1.0-beta.3",
"husky": "^4.2.5",
"jest": "^25.1.0",
"less": "^3.11.1",
"lint-staged": "^10.2.11",
"log-symbols": "^3.0.0",
"node-abi": "^2.18.0",
"node-fetch": "^2.6.1",
"node-abi": "^2.15.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"prettier": "^2.0.5",
"react-test-renderer": "^16.13.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^13.2.0",
"stylelint-config-standard": "^20.0.0",
"ts-jest": "^25.2.1",
"tslint": "^6.0.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.2.0",
"typescript": "^3.8.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"./**/*.{js,ts,tsx}": [
"eslint --fix"
],
"./static/show-me/**/*.js": [
"standard --fix"
],
"./src/less/*.less": [
"stylelint --fix"
]
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ declare global {
interface Window {
ElectronFiddle: {
app: AppType;
contentChangeListeners: Array<any>;
contentChangeListeners: Array<any>,
editors: Record<EditorId, MonacoType.editor.IStandaloneCodeEditor | null>;
};
}
+1
View File
@@ -18,3 +18,4 @@ export const html = `<!DOCTYPE html>
</script>
</body>
</html>`;
+1 -1
View File
@@ -21,7 +21,7 @@ function createWindow () {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow)
app.on('ready', createWindow)
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
+7 -29
View File
@@ -8,27 +8,13 @@ export enum VersionState {
ready = 'ready',
downloading = 'downloading',
unzipping = 'unzipping',
unknown = 'unknown',
unknown = 'unknown'
}
export enum VersionSource {
remote = 'remote',
local = 'local',
local = 'local'
}
export enum GistActionType {
publish = 'Publish',
update = 'Update',
delete = 'Delete',
}
export enum GistActionState {
publishing = 'publishing',
updating = 'updating',
deleting = 'deleting',
none = 'none',
}
export interface Version {
version: string;
name?: string;
@@ -71,9 +57,7 @@ export interface GenericDialogOptions {
type: GenericDialogType;
ok?: string;
cancel?: string;
wantsInput?: boolean;
label: string | JSX.Element;
placeholder?: string;
label: string;
}
export interface Templates {
@@ -92,23 +76,17 @@ export const enum EditorId {
'renderer' = 'renderer',
'html' = 'html',
'preload' = 'preload',
'css' = 'css',
'css' = 'css'
}
// Panels that can show up as a mosaic
export const enum PanelId {
'docsDemo' = 'docsDemo',
'docsDemo' = 'docsDemo'
}
export type MosaicId = EditorId | PanelId;
export const ALL_EDITORS = [
EditorId.main,
EditorId.renderer,
EditorId.preload,
EditorId.html,
EditorId.css,
];
export const ALL_EDITORS = [EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html, EditorId.css];
export const ALL_PANELS = [PanelId.docsDemo];
export const ALL_MOSAICS = [...ALL_EDITORS, ...ALL_PANELS];
@@ -116,5 +94,5 @@ export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
export const enum DocsDemoPage {
DEFAULT = 'DEFAULT',
DEMO_APP = 'DEMO_APP',
DEMO_APP = 'DEMO_APP'
}
+1 -4
View File
@@ -27,7 +27,6 @@ export enum IpcEvents {
BEFORE_QUIT = 'BEFORE_QUIT',
CONFIRM_QUIT = 'CONFIRM_QUIT',
SET_APPDATA_DIR = 'SET_APPDATA_DIR',
SELECT_ALL_IN_EDITOR = 'SELECT_ALL_IN_EDITOR',
}
export const ipcMainEvents = [
@@ -62,8 +61,6 @@ export const ipcRendererEvents = [
IpcEvents.BISECT_COMMANDS_TOGGLE,
IpcEvents.BEFORE_QUIT,
IpcEvents.SET_APPDATA_DIR,
IpcEvents.SELECT_ALL_IN_EDITOR,
];
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL =
'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
+42 -54
View File
@@ -1,9 +1,4 @@
import {
BrowserWindow,
ContextMenuParams,
Menu,
MenuItemConstructorOptions,
} from 'electron';
import { BrowserWindow, ContextMenuParams, Menu, MenuItemConstructorOptions } from 'electron';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
@@ -18,16 +13,16 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
{
id: 'run',
label: 'Run Fiddle',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
id: 'clear_console',
label: 'Clear Console',
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE),
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
},
{
type: 'separator',
},
type: 'separator'
}
];
}
@@ -40,10 +35,9 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
* @param {ContextMenuParams} { x, y }
* @returns {Array<MenuItemConstructorOptions>}
*/
export function getMonacoItems({
pageURL,
editFlags,
}: ContextMenuParams): Array<MenuItemConstructorOptions> {
export function getMonacoItems(
{ pageURL, editFlags }: ContextMenuParams
): Array<MenuItemConstructorOptions> {
if (!editFlags.canPaste || !/.*index\.html(#?)$/.test(pageURL || '')) {
return [];
}
@@ -53,51 +47,51 @@ export function getMonacoItems({
id: 'go_to_definition',
label: 'Go to Definition',
click() {
const cmd = ['editor.action.goToDeclaration'];
const cmd = [ 'editor.action.goToDeclaration' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'peek_definition',
label: 'Peek Definition',
click() {
const cmd = ['editor.action.previewDeclaration'];
const cmd = [ 'editor.action.previewDeclaration' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'references',
label: 'Find References',
click() {
const cmd = ['editor.action.referenceSearch.trigger'];
const cmd = [ 'editor.action.referenceSearch.trigger' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
{
id: 'palette',
label: 'Command Palette',
click() {
const cmd = ['editor.action.quickCommand'];
const cmd = [ 'editor.action.quickCommand' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
{
id: 'format_document',
label: 'Format Document',
click() {
const cmd = ['editor.action.formatDocument'];
const cmd = [ 'editor.action.formatDocument' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'format_selection',
label: 'Format Selection',
click() {
const cmd = ['editor.action.formatSelection'];
const cmd = [ 'editor.action.formatSelection' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
];
@@ -111,28 +105,25 @@ export function getMonacoItems({
* @returns {Array<MenuItemConstructorOptions>}
*/
export function getInspectItems(
browserWindow: BrowserWindow,
{ x, y }: ContextMenuParams,
browserWindow: BrowserWindow, { x, y }: ContextMenuParams
): Array<MenuItemConstructorOptions> {
if (!isDevMode()) return [];
return [
{
id: 'inspect',
label: 'Inspect Element',
click: () => {
browserWindow.webContents.inspectElement(x, y);
return [{
id: 'inspect',
label: 'Inspect Element',
click: () => {
browserWindow.webContents.inspectElement(x, y);
try {
if (browserWindow.webContents.isDevToolsOpened()) {
browserWindow.webContents.devToolsWebContents?.focus();
}
} catch (error) {
console.warn(`Tried to focus dev tools, but failed`, { error });
try {
if (browserWindow.webContents.isDevToolsOpened()) {
browserWindow.webContents.devToolsWebContents.focus();
}
},
},
];
} catch (error) {
console.warn(`Tried to focus dev tools, but failed`, { error });
}
}
}];
}
/**
@@ -151,24 +142,21 @@ export function createContextMenu(browserWindow: BrowserWindow) {
id: 'cut',
label: 'Cut',
role: 'cut',
enabled: editFlags.canCut,
},
{
enabled: editFlags.canCut
}, {
id: 'copy',
label: 'Copy',
role: 'copy',
enabled: editFlags.canCopy,
},
{
enabled: editFlags.canCopy
}, {
id: 'paste',
label: 'Paste',
role: 'paste',
enabled: editFlags.canPaste,
enabled: editFlags.canPaste
}, {
type: 'separator'
},
{
type: 'separator',
},
...getInspectItems(browserWindow, props),
...getInspectItems(browserWindow, props)
];
const menu = Menu.buildFromTemplate(template);
+1 -1
View File
@@ -12,7 +12,7 @@ export async function setupDevTools(): Promise<void> {
const {
default: installExtension,
REACT_DEVELOPER_TOOLS,
REACT_PERF,
REACT_PERF
} = require('electron-devtools-installer');
try {
+7 -9
View File
@@ -17,12 +17,10 @@ export function setupDialogs() {
showConfirmationDialog(args);
});
ipcMainManager.on(
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG,
async (event) => {
await showOpenDialog(event);
},
);
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
await showOpenDialog(event);
});
}
/**
@@ -33,7 +31,7 @@ export function setupDialogs() {
function showWarningDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args,
...args
});
}
@@ -45,14 +43,14 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args,
...args
});
}
async function showOpenDialog(event: IpcMainEvent) {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Folder',
properties: ['openDirectory'],
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
+7 -12
View File
@@ -3,12 +3,7 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import { IpcEvents } from '../ipc-events';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PACKAGE_NAME,
RENDERER_JS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { ipcMainManager } from './ipc';
/**
@@ -27,14 +22,14 @@ export function setupFileListeners() {
export async function showOpenDialog() {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Fiddle',
properties: ['openDirectory'],
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [filePaths[0]]);
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
}
/**
@@ -46,7 +41,7 @@ export async function showSaveDialog(event?: IpcEvents, as?: string) {
const { filePaths } = await dialog.showOpenDialog({
buttonLabel: 'Save here',
properties: ['openDirectory', 'createDirectory'],
title: `Save Fiddle${as ? ` as ${as}` : ''}`,
title: `Save Fiddle${as ? ` as ${as}` : ''}`
});
if (!filePaths || filePaths.length < 1) {
@@ -57,7 +52,7 @@ export async function showSaveDialog(event?: IpcEvents, as?: string) {
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [filePaths[0]]);
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
}
@@ -72,7 +67,7 @@ async function ensureSaveTargetEmpty(filePath: string): Promise<boolean> {
path.join(filePath, INDEX_HTML_NAME),
path.join(filePath, RENDERER_JS_NAME),
path.join(filePath, MAIN_JS_NAME),
path.join(filePath, PACKAGE_NAME),
path.join(filePath, PACKAGE_NAME)
];
let noFilesOrOverwriteGranted = true;
@@ -97,7 +92,7 @@ async function confirmFileOverwrite(filePath: string): Promise<boolean> {
try {
const result = await dialog.showMessageBox({
type: 'warning',
buttons: ['Cancel', 'Yes'],
buttons: [ 'Cancel', 'Yes' ],
message: 'Overwrite files?',
detail: `The file ${filePath} already exists. Do you want to overwrite it?`,
});
+12 -26
View File
@@ -1,11 +1,7 @@
import { ipcMain } from 'electron';
import { EventEmitter } from 'events';
import {
IpcEvents,
ipcMainEvents,
WEBCONTENTS_READY_FOR_IPC_SIGNAL,
} from '../ipc-events';
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { getOrCreateMainWindow } from './windows';
/**
@@ -18,10 +14,7 @@ import { getOrCreateMainWindow } from './windows';
*/
export class IpcMainManager extends EventEmitter {
public readyWebContents = new WeakSet<Electron.WebContents>();
private messageQueue = new WeakMap<
Electron.WebContents,
Array<[IpcEvents, Array<any> | undefined]>
>();
private messageQueue = new WeakMap<Electron.WebContents, Array<[IpcEvents, Array<any> | undefined]>>();
constructor() {
super();
@@ -31,19 +24,16 @@ export class IpcMainManager extends EventEmitter {
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
ipcMain.on(
WEBCONTENTS_READY_FOR_IPC_SIGNAL,
(event: Electron.IpcMainEvent) => {
this.readyWebContents.add(event.sender);
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.IpcMainEvent) => {
this.readyWebContents.add(event.sender);
const queue = this.messageQueue.get(event.sender);
this.messageQueue.delete(event.sender);
if (!queue) return;
for (const item of queue) {
this.send(item[0], item[1], event.sender);
}
},
);
const queue = this.messageQueue.get(event.sender);
this.messageQueue.delete(event.sender);
if (!queue) return;
for (const item of queue) {
this.send(item[0], item[1], event.sender);
}
});
}
/**
@@ -54,11 +44,7 @@ export class IpcMainManager extends EventEmitter {
* @param {Array<any>} args
* @param {Electron.WebContents} [target]
*/
public send(
channel: IpcEvents,
args?: Array<any>,
target?: Electron.WebContents,
) {
public send(channel: IpcEvents, args?: Array<any>, target?: Electron.WebContents) {
const _target = target || getOrCreateMainWindow().webContents;
const _args = args || [];
if (!this.readyWebContents.has(_target)) {
+3 -6
View File
@@ -5,7 +5,7 @@ import { app } from 'electron';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { setupAboutPanel } from './about-panel';
import { setupAboutPanel } from '../utils/set-about-panel';
import { setupDevTools } from './devtools';
import { setupDialogs } from './dialogs';
import { onFirstRunMaybe } from './first-run';
@@ -47,10 +47,7 @@ export function onBeforeQuit() {
ipcMainManager.on(IpcEvents.CONFIRM_QUIT, quitAppIfConfirmed);
}
export function quitAppIfConfirmed(
_: Electron.IpcMainEvent,
quitConfirmed: boolean,
) {
export function quitAppIfConfirmed(_: Electron.IpcMainEvent, quitConfirmed: boolean) {
if (quitConfirmed) {
app.quit();
}
@@ -89,7 +86,7 @@ export function main() {
listenForProtocolHandler();
// Launch
app.whenReady().then(onReady);
app.on('ready', onReady);
app.on('before-quit', onBeforeQuit);
app.on('window-all-closed', onWindowsAllClosed);
app.on('activate', getOrCreateMainWindow);
+83 -136
View File
@@ -1,10 +1,4 @@
import {
app,
BrowserWindow,
Menu,
MenuItemConstructorOptions,
shell,
} from 'electron';
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
import { Templates } from '../interfaces';
import { IpcEvents } from '../ipc-events';
@@ -20,7 +14,7 @@ import { createMainWindow } from './windows';
* @returns {submenu is Array<Electron.MenuItemConstructorOptions>}
*/
function isSubmenu(
submenu?: Array<MenuItemConstructorOptions> | Menu,
submenu?: Array<MenuItemConstructorOptions> | Menu
): submenu is Array<MenuItemConstructorOptions> {
return !!submenu && Array.isArray(submenu);
}
@@ -33,16 +27,16 @@ function isSubmenu(
function getHelpItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
type: 'separator'
},
{
label: 'Show Welcome Tour',
click() {
ipcMainManager.send(IpcEvents.SHOW_WELCOME_TOUR);
},
}
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Toggle Developer Tools',
@@ -51,30 +45,30 @@ function getHelpItems(): Array<MenuItemConstructorOptions> {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
browserWindow.webContents.toggleDevTools();
browserWindow.webContents.openDevTools({ mode: 'bottom' });
}
},
}
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Open Fiddle Repository...',
click() {
shell.openExternal('https://github.com/electron/fiddle');
},
}
},
{
label: 'Open Electron Repository...',
click() {
shell.openExternal('https://github.com/electron/electron');
},
}
},
{
label: 'Open Electron Issue Tracker...',
click() {
shell.openExternal('https://github.com/electron/electron/issues');
},
}
},
];
}
@@ -88,18 +82,16 @@ function getHelpItems(): Array<MenuItemConstructorOptions> {
function getPreferencesItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
},
{
type: 'separator'
}, {
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click() {
ipcMainManager.send(IpcEvents.OPEN_SETTINGS);
},
},
{
type: 'separator',
},
}
}, {
type: 'separator'
}
];
}
@@ -111,11 +103,10 @@ function getPreferencesItems(): Array<MenuItemConstructorOptions> {
function getQuitItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
},
{
role: 'quit',
},
type: 'separator'
}, {
role: 'quit'
}
];
}
@@ -129,32 +120,29 @@ function getTasksMenu(): MenuItemConstructorOptions {
{
label: 'Run Fiddle...',
accelerator: 'F5',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
label: 'Package Fiddle...',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_PACKAGE),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_PACKAGE)
},
{
label: 'Make installers for Fiddle...',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_MAKE),
},
click: () => ipcMainManager.send(IpcEvents.FIDDLE_MAKE)
}
];
return {
label: 'Tasks',
submenu: tasksMenu,
submenu: tasksMenu
};
}
function getShowMeMenuItem(
key: string,
item: string | Templates,
): MenuItemConstructorOptions {
function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConstructorOptions {
if (typeof item === 'string') {
return {
label: key,
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key]),
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
};
}
@@ -162,18 +150,17 @@ function getShowMeMenuItem(
label: key,
submenu: Object.keys(item).map((subkey) => {
return getShowMeMenuItem(subkey, item[subkey]);
}),
})
};
}
function getShowMeMenu(): MenuItemConstructorOptions {
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(
SHOW_ME_TEMPLATES,
).map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
return {
label: 'Show Me',
submenu: showMeMenu,
submenu: showMeMenu
};
}
@@ -187,36 +174,34 @@ function getFileMenu(): MenuItemConstructorOptions {
{
label: 'New Fiddle',
click: () => ipcMainManager.send(IpcEvents.FS_NEW_FIDDLE),
accelerator: 'CmdOrCtrl+N',
},
{
accelerator: 'CmdOrCtrl+N'
}, {
label: 'New Window',
click: () => createMainWindow(),
accelerator: 'CmdOrCtrl+Shift+N',
},
{
type: 'separator',
accelerator: 'CmdOrCtrl+Shift+N'
}, {
type: 'separator'
},
{
label: 'Open',
click: showOpenDialog,
accelerator: 'CmdOrCtrl+O',
accelerator: 'CmdOrCtrl+O'
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Save',
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE),
accelerator: 'CmdOrCtrl+S',
accelerator: 'CmdOrCtrl+S'
},
{
label: 'Save as',
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE),
accelerator: 'CmdOrCtrl+Shift+S',
accelerator: 'CmdOrCtrl+Shift+S'
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Publish to Gist',
@@ -224,24 +209,18 @@ function getFileMenu(): MenuItemConstructorOptions {
},
{
label: 'Save as Forge Project',
click: () =>
showSaveDialog(IpcEvents.FS_SAVE_FIDDLE_FORGE, 'Forge Project'),
},
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE_FORGE, 'Forge Project')
}
];
// macOS has these items in the "Fiddle" menu
if (process.platform !== 'darwin') {
fileMenu.splice(
fileMenu.length,
0,
...getPreferencesItems(),
...getQuitItems(),
);
fileMenu.splice(fileMenu.length, 0, ...getPreferencesItems(), ...getQuitItems());
}
return {
label: 'File',
submenu: fileMenu,
submenu: fileMenu
};
}
@@ -251,85 +230,53 @@ function getFileMenu(): MenuItemConstructorOptions {
export function setupMenu() {
// Get template for default menu
const defaultMenu = require('electron-default-menu');
const menu = (defaultMenu(app, shell) as Array<
MenuItemConstructorOptions
>).map((item) => {
const { label } = item;
const menu = (defaultMenu(app, shell) as Array<MenuItemConstructorOptions>)
.map((item) => {
const { label } = item;
// Append the "Settings" item
if (
process.platform === 'darwin' &&
label === app.name &&
isSubmenu(item.submenu)
) {
item.submenu.splice(2, 0, ...getPreferencesItems());
}
// Append the "Settings" item
if (
process.platform === 'darwin'
&& label === app.name
&& isSubmenu(item.submenu)
) {
item.submenu.splice(2, 0, ...getPreferencesItems());
}
// Custom handler for "Select All" for Monaco
if (label === 'Edit' && isSubmenu(item.submenu)) {
const selectAll = item.submenu.find((i) => i.label === 'Select All')!;
delete selectAll.role; // override default role
selectAll.click = () => {
ipcMainManager.send(IpcEvents.SELECT_ALL_IN_EDITOR);
// Allow selection to occur in text fields outside the editors.
if (process.platform === 'darwin') {
Menu.sendActionToFirstResponder('selectAll:');
}
};
}
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
item.submenu = item.submenu.filter(
(subItem) =>
subItem.label !== 'Toggle Developer Tools' &&
subItem.label !== 'Reload',
);
item.submenu.push(
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
); // Add zooming actions
item.submenu.push(
{ type: 'separator' },
{
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
item.submenu = item.submenu
.filter((subItem) => subItem.label !== 'Toggle Developer Tools' && subItem.label !== 'Reload');
item.submenu.push({ type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }); // Add zooming actions
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Soft Wrap',
click: () =>
ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
},
);
item.submenu.push(
{ type: 'separator' },
{
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Mini Map',
click: () =>
ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [
'minimap.enabled',
]),
},
);
item.submenu.push(
{ type: 'separator' },
{
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['minimap.enabled']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Bisect Helper',
click: () => ipcMainManager.send(IpcEvents.BISECT_COMMANDS_TOGGLE),
accelerator: 'CommandorControl+Shift+B',
},
);
}
});
}
// Append items to "Help"
if (label === 'Help' && isSubmenu(item.submenu)) {
item.submenu = getHelpItems();
}
// Append items to "Help"
if (label === 'Help' && isSubmenu(item.submenu)) {
item.submenu = getHelpItems();
}
return item;
});
return item;
});
menu.splice(process.platform === 'darwin' ? 1 : 0, 0, getFileMenu());
menu.splice(
process.platform === 'darwin' ? 1 : 0,
0,
getFileMenu()
);
menu.splice(menu.length - 1, 0, getTasksMenu(), getShowMeMenu());
+11 -21
View File
@@ -8,11 +8,7 @@ import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
const PROTOCOL = 'electron-fiddle';
const squirrelPath = path.resolve(
path.dirname(process.execPath),
'..',
'electron-fiddle.exe',
);
const squirrelPath = path.resolve(path.dirname(process.execPath), '..', 'electron-fiddle.exe');
const handlePotentialProtocolLaunch = (url: string) => {
if (!app.isReady()) {
@@ -30,18 +26,14 @@ const handlePotentialProtocolLaunch = (url: string) => {
case 'gist':
if (pathParts.length === 1) {
// We only have a gist ID
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [
{
id: pathParts[0],
},
]);
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
id: pathParts[0],
}]);
} else if (pathParts.length === 2) {
// We have a gist owner and gist ID, we can ignore the owner
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [
{
id: pathParts[1],
},
]);
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
id: pathParts[1],
}]);
} else {
// This is a super invalid gist launch
return;
@@ -52,12 +44,10 @@ const handlePotentialProtocolLaunch = (url: string) => {
if (pathParts.length > 1) {
// First part of the commit HASH / ref / branch
// Rest is the path to the example
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [
{
ref: pathParts[0],
path: pathParts.slice(1).join('/'),
},
]);
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [{
ref: pathParts[0],
path: pathParts.slice(1).join('/'),
}]);
} else {
// This is an invalid electron launch
return;
+7 -11
View File
@@ -23,11 +23,12 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
backgroundColor: '#1d2427',
webPreferences: {
webviewTag: false,
nodeIntegration: true,
},
nodeIntegration: true
}
};
}
/**
* Creates a new main window.
*
@@ -49,7 +50,8 @@ export function createMainWindow(): Electron.BrowserWindow {
});
browserWindow.on('closed', () => {
browserWindows = browserWindows.filter((bw) => browserWindow !== bw);
browserWindows = browserWindows
.filter((bw) => browserWindow !== bw);
browserWindow = null;
});
@@ -71,11 +73,7 @@ export function createMainWindow(): Electron.BrowserWindow {
});
const appData = app.getPath('appData');
ipcMainManager.send(
IpcEvents.SET_APPDATA_DIR,
[appData],
browserWindow.webContents,
);
ipcMainManager.send(IpcEvents.SET_APPDATA_DIR, [appData], browserWindow.webContents);
browserWindows.push(browserWindow);
@@ -88,7 +86,5 @@ export function createMainWindow(): Electron.BrowserWindow {
* @returns {Electron.BrowserWindow}
*/
export function getOrCreateMainWindow(): Electron.BrowserWindow {
return (
BrowserWindow.getFocusedWindow() || browserWindows[0] || createMainWindow()
);
return BrowserWindow.getFocusedWindow() || browserWindows[0] || createMainWindow();
}
+8 -8
View File
@@ -10,7 +10,7 @@ import {
EditorId,
EditorValues,
GenericDialogType,
SetFiddleOptions,
SetFiddleOptions
} from '../interfaces';
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { updateEditorLayout } from '../utils/editor-layout';
@@ -52,14 +52,14 @@ export class App {
public async replaceFiddle(
editorValues: Partial<EditorValues>,
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>,
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>
) {
// if unsaved, prompt user to make sure they're okay with overwriting and changing directory
if (this.state.isUnsaved) {
this.state.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes',
ok: 'Yes'
});
this.state.isGenericDialogShowing = true;
await when(() => !this.state.isGenericDialogShowing);
@@ -80,6 +80,8 @@ export class App {
}
}
document.title = getTitle(this.state);
this.state.gistId = gistId || '';
this.state.localPath = filePath;
this.state.templateName = templateName;
@@ -89,8 +91,6 @@ export class App {
await this.setEditorValues(editorValues);
this.state.isUnsaved = false;
document.title = getTitle(this.state);
return true;
}
@@ -135,7 +135,7 @@ export class App {
* @returns {EditorValues}
*/
public async getEditorValues(
options?: PackageJsonOptions,
options?: PackageJsonOptions
): Promise<EditorValues> {
const { ElectronFiddle: fiddle } = window;
@@ -148,7 +148,7 @@ export class App {
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
@@ -198,7 +198,7 @@ export class App {
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme',
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
+19 -35
View File
@@ -15,10 +15,7 @@ import { AppState } from './state';
* @param {string} iVersion
* @returns {Promise<void>}
*/
export async function setupBinary(
appState: AppState,
iVersion: string,
): Promise<void> {
export async function setupBinary(appState: AppState, iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
@@ -41,9 +38,7 @@ export async function setupBinary(
const zipPath = await download(appState, version);
const extractPath = getDownloadPath(version);
console.log(
`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`,
);
console.log(`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`);
try {
appState.versions[version].state = VersionState.unzipping;
@@ -77,14 +72,11 @@ export async function removeBinary(iVersion: string) {
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
const rerunner = async (func: () => Promise<void>, counter = 1) => {
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
try {
await func();
} catch (error) {
console.warn(
`Binary Manager: failed to run ${func.name} for ${version}, but failed`,
error,
);
console.warn(`Binary Manager: failed to run ${func.name} for ${version}, but failed`, error);
if (counter < 4) {
console.log(`Binary Manager: Trying again to run ${func.name}`);
await rerunner(func, counter + 1);
@@ -117,19 +109,16 @@ export async function removeBinary(iVersion: string) {
}
/* Did we already download a given version?
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
export async function getIsDownloaded(
version: string,
dir?: string,
): Promise<boolean> {
const expectedPath = getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
export async function getIsDownloaded(version: string, dir?: string): Promise<boolean> {
const expectedPath = getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(expectedPath);
return fs.existsSync(expectedPath);
}
/**
@@ -152,9 +141,7 @@ export function getElectronBinaryPath(
case 'win32':
return path.join(dir, 'electron.exe');
default:
throw new Error(
`Electron builds are not available for ${process.platform}`,
);
throw new Error(`Electron builds are not available for ${process.platform}`);
}
}
@@ -164,6 +151,7 @@ export function getDownloadingVersions(appState: AppState) {
.map(([version, _]) => version);
}
/**
* Returns an array of all versions downloaded to disk
*
@@ -204,24 +192,20 @@ async function download(appState: AppState, version: string): Promise<string> {
const roundedProgress = Math.round(progress.percent * 100) / 100;
if (roundedProgress !== appState.versions[version].downloadProgress) {
console.debug(
`Binary: Version ${version} download progress: ${progress.percent}`,
);
console.debug(`Binary: Version ${version} download progress: ${progress.percent}`);
appState.versions[version].downloadProgress = roundedProgress;
}
};
if (!appState.versions[version]) {
throw new Error(
`Version ${version} does not exist in state, cannot download`,
);
throw new Error(`Version ${version} does not exist in state, cannot download`);
}
const zipFilePath = await electronDownload(version, {
downloadOptions: {
quiet: true,
getProgressCallback,
},
getProgressCallback
}
});
return zipFilePath;
+1 -2
View File
@@ -36,8 +36,7 @@ export class Bisector {
isBisectOver = true;
}
} else {
const downPivot =
Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
const downPivot = Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
this.maxRev = this.pivot;
if (downPivot !== this.minRev && downPivot !== this.pivot) {
this.pivot = downPivot;
+3 -6
View File
@@ -12,10 +12,7 @@ export interface ChromeMacProps {
@observer
export class ChromeMac extends React.Component<ChromeMacProps> {
public handleDoubleClick = () => {
const doubleClickAction = remote.systemPreferences.getUserDefault(
'AppleActionOnDoubleClick',
'string',
);
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
const win = remote.getCurrentWindow();
if (doubleClickAction === 'Minimize') {
win.minimize();
@@ -26,13 +23,13 @@ export class ChromeMac extends React.Component<ChromeMacProps> {
win.unmaximize();
}
}
};
}
public render() {
if (process.platform !== 'darwin') return null;
return (
<div className="chrome drag" onDoubleClick={this.handleDoubleClick}>
<div className='chrome drag' onDoubleClick={this.handleDoubleClick}>
<small>{getTitle(this.props.appState)}</small>
</div>
);
@@ -1,440 +0,0 @@
import {
Button,
ButtonGroup,
IToastProps,
Menu,
MenuItem,
Popover,
Position,
Toaster,
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { when } from 'mobx';
import {
EditorValues,
GenericDialogType,
GistActionState,
GistActionType,
} from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../../shared-constants';
import { getOctokit } from '../../utils/octokit';
import { EMPTY_EDITOR_CONTENT } from '../constants';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface GistActionButtonProps {
appState: AppState;
}
interface IGistActionButtonState {
readonly isUpdating: boolean;
readonly isDeleting: boolean;
readonly actionType: GistActionType;
}
/**
* The "publish" button takes care of logging you in.
*
* @export
* @class GistActionButton
* @extends {React.Component<GistActionButtonProps, GistActionButtonState>}
*/
@observer
export class GistActionButton extends React.Component<
GistActionButtonProps,
IGistActionButtonState
> {
public constructor(props: GistActionButtonProps) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.performGistAction = this.performGistAction.bind(this);
this.setPrivate = this.setPrivate.bind(this);
this.setPublic = this.setPublic.bind(this);
this.state = {
isUpdating: false,
isDeleting: false,
actionType: GistActionType.publish,
};
}
private toaster: Toaster;
private refHandlers = {
toaster: (ref: Toaster) => (this.toaster = ref),
};
public componentDidMount() {
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
public componentWillUnmount() {
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
/**
* When the user clicks the publish button, we either show the
* authentication dialog or publish right away.
*
* If we're showing the authentication dialog, we wait for it
* to be closed again (or a GitHub token to show up) before
* we publish
*
* @returns {Promise<void>}
* @memberof GistActionButton
*/
public async handleClick(): Promise<void> {
const { appState } = this.props;
if (!appState.gitHubToken) {
appState.toggleAuthDialog();
}
// Wait for the dialog to be closed again
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
if (appState.gitHubToken) {
return this.performGistAction();
}
}
public async getFiddleDescriptionFromUser(): Promise<string> {
const { appState } = this.props;
// Reset potentially non-null last description.
appState.genericDialogLastInput = null;
appState.setGenericDialogOptions({
type: GenericDialogType.confirm,
label: 'Please provide a brief description for your Fiddle Gist',
wantsInput: true,
ok: 'Publish',
cancel: undefined,
placeholder: 'Electron Fiddle Gist',
});
appState.isGenericDialogShowing = true;
await when(() => !appState.isGenericDialogShowing);
return appState.genericDialogLastInput ?? 'Electron Fiddle Gist';
}
/**
* Publish a new GitHub gist.
*/
public async handlePublish() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getEditorValues(options);
appState.activeGistAction = GistActionState.publishing;
try {
const description = await this.getFiddleDescriptionFromUser();
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description,
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
});
appState.gistId = gist.data.id;
appState.localPath = undefined;
console.log(`Publish Button: Publishing complete`, { gist });
this.renderToast({ message: 'Publishing completed successfully!' });
// Only set action type to update if publish completed successfully.
this.setActionType(GistActionType.update);
} catch (error) {
console.warn(`Could not publish gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
// Ensure any previous input is reset.
appState.genericDialogLastInput = null;
appState.activeGistAction = GistActionState.none;
}
/**
* Update an existing GitHub gist.
*/
public async handleUpdate() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getEditorValues(options);
appState.activeGistAction = GistActionState.updating;
try {
const gist = await octo.gists.update({
gist_id: appState.gistId!,
files: this.gistFilesList(values) as any,
});
console.log('Updating: Updating done', { gist });
this.renderToast({ message: 'Successfully updated gist!' });
} catch (error) {
console.warn(`Could not update gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Updating Fiddle Gist failed. Are you connected to the Internet and is this your Gist?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.activeGistAction = GistActionState.none;
this.setActionType(GistActionType.update);
}
/**
* Delete an existing GitHub gist.
*/
public async handleDelete() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
appState.activeGistAction = GistActionState.deleting;
try {
const gist = await octo.gists.delete({
gist_id: appState.gistId!,
});
console.log('Deleting: Deleting done', { gist });
this.renderToast({ message: 'Successfully deleted gist!' });
} catch (error) {
console.warn(`Could not delete gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Deleting Fiddle Gist failed. Are you connected to the Internet, is this your Gist, and have you loaded it?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.gistId = undefined;
appState.activeGistAction = GistActionState.none;
this.setActionType(GistActionType.publish);
}
/**
* Connect with GitHub, perform a publish/update/delete action,
* and update all related properties in the app state.
*/
public async performGistAction(): Promise<void> {
const { gistId } = this.props.appState;
const { actionType } = this.state;
if (gistId) {
switch (actionType) {
case GistActionType.publish:
await this.handlePublish();
break;
case GistActionType.update:
await this.handleUpdate();
break;
case GistActionType.delete:
await this.handleDelete();
break;
}
} else {
await this.handlePublish();
}
}
/**
* Publish fiddles as private.
*
* @memberof GistActionButton
*/
public setPrivate() {
this.setPrivacy(false);
}
/**
* Publish fiddles as public.
*
* @memberof GistActionButton
*/
public setPublic() {
this.setPrivacy(true);
}
public render() {
const { gistId, activeGistAction } = this.props.appState;
const { actionType } = this.state;
const getTextForButton = () => {
let text;
if (gistId) {
text = actionType;
} else if (activeGistAction === GistActionState.updating) {
text = 'Updating...';
} else if (activeGistAction === GistActionState.publishing) {
text = 'Publishing...';
} else if (activeGistAction === GistActionState.deleting) {
text = 'Deleting...';
} else {
text = 'Publish';
}
return text;
};
const getActionIcon = () => {
switch (actionType) {
case GistActionType.publish:
return 'upload';
case GistActionType.update:
return 'refresh';
case GistActionType.delete:
return 'delete';
}
};
const isPerformingAction = activeGistAction !== GistActionState.none;
return (
<>
<fieldset disabled={isPerformingAction}>
<ButtonGroup className="button-gist-action">
{this.renderPrivacyMenu()}
<Button
onClick={this.handleClick}
loading={isPerformingAction}
icon={getActionIcon()}
text={getTextForButton()}
/>
{this.renderGistActionMenu()}
</ButtonGroup>
</fieldset>
<Toaster
position={Position.BOTTOM_RIGHT}
ref={this.refHandlers.toaster}
/>
</>
);
}
private renderGistActionMenu = () => {
const { gistId } = this.props.appState;
const { actionType } = this.state;
if (!gistId) {
return null;
}
const menu = (
<Menu>
<MenuItem
text="Publish"
active={actionType === GistActionType.publish}
onClick={() => this.setActionType(GistActionType.publish)}
/>
<MenuItem
text="Update"
active={actionType === GistActionType.update}
onClick={() => this.setActionType(GistActionType.update)}
/>
<MenuItem
text="Delete"
active={actionType === GistActionType.delete}
onClick={() => this.setActionType(GistActionType.delete)}
/>
</Menu>
);
return (
<Popover content={menu} position={Position.BOTTOM}>
<Button icon="wrench" />
</Popover>
);
};
private renderPrivacyMenu = () => {
const { gitHubPublishAsPublic, gistId } = this.props.appState;
if (gistId) {
return null;
}
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
<Menu>
<MenuItem
text="Private"
icon="lock"
active={!gitHubPublishAsPublic}
onClick={this.setPrivate}
/>
<MenuItem
text="Public"
icon="unlock"
active={gitHubPublishAsPublic}
onClick={this.setPublic}
/>
</Menu>
);
return (
<Popover content={privacyMenu} position={Position.BOTTOM}>
<Button icon={privacyIcon} />
</Popover>
);
};
private setActionType = (actionType: GistActionType) => {
this.setState({ actionType });
};
private setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
}
private renderToast = (toast: IToastProps) => {
this.toaster.show(toast);
};
private gistFilesList = (values: EditorValues) => {
return {
[INDEX_HTML_NAME]: {
content: values.html || EMPTY_EDITOR_CONTENT.html,
},
[MAIN_JS_NAME]: {
content: values.main || EMPTY_EDITOR_CONTENT.js,
},
[RENDERER_JS_NAME]: {
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
},
[PRELOAD_JS_NAME]: {
content: values.preload || EMPTY_EDITOR_CONTENT.js,
},
[STYLES_CSS_NAME]: {
content: values.css || EMPTY_EDITOR_CONTENT.css,
},
};
};
}
@@ -5,7 +5,6 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { IpcEvents } from '../../ipc-events';
import { GistActionState } from '../../interfaces';
import { idFromUrl, urlFromId } from '../../utils/gist';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -23,10 +22,7 @@ export interface AddressBarState {
}
@observer
export class AddressBar extends React.Component<
AddressBarProps,
AddressBarState
> {
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
constructor(props: AddressBarProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
@@ -34,7 +30,7 @@ export class AddressBar extends React.Component<
this.submit = this.submit.bind(this);
const { gistId } = this.props.appState;
const value = urlFromId(gistId);
const value = gistId ? urlFromId(gistId) : '';
const { remoteLoader } = window.ElectronFiddle.app;
@@ -42,8 +38,8 @@ export class AddressBar extends React.Component<
value,
loaders: {
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader),
},
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
}
};
}
@@ -66,9 +62,7 @@ export class AddressBar extends React.Component<
public submit() {
const { remoteLoader } = window.ElectronFiddle.app;
if (this.state.value) {
remoteLoader.fetchGistAndLoad(
idFromUrl(this.state.value) || this.state.value,
);
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
}
}
@@ -80,25 +74,16 @@ export class AddressBar extends React.Component<
const { loaders } = this.state;
reaction(
() => appState.gistId,
(gistId: string) => this.setState({ value: urlFromId(gistId) }),
(gistId: string) => this.setState({ value: urlFromId(gistId) })
);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.on(
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
loaders.example,
);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
public componentWillUnmount() {
const { loaders } = this.state;
ipcRendererManager.removeListener(
IpcEvents.LOAD_GIST_REQUEST,
loaders.gist,
);
ipcRendererManager.removeListener(
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
loaders.example,
);
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
/**
@@ -114,29 +99,28 @@ export class AddressBar extends React.Component<
return (
<Button
disabled={!isValueCorrect}
icon="cloud-download"
text="Load Fiddle"
icon='cloud-download'
text='Load Fiddle'
onClick={this.submit}
/>
);
}
public render() {
const { isUnsaved, activeGistAction } = this.props.appState;
const { isUnsaved, isPublishing } = this.props.appState;
const { value } = this.state;
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
const className = classnames('address-bar', isUnsaved, { empty: !value });
const isPerformingAction = activeGistAction !== GistActionState.none;
return (
<form className={className} onSubmit={this.handleSubmit}>
<fieldset disabled={isPerformingAction}>
<fieldset disabled={isPublishing}>
<InputGroup
key="addressbar"
leftIcon="geosearch"
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder="https://gist.github.com/..."
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
+11 -21
View File
@@ -29,25 +29,13 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
const [minRev, maxRev] = response;
const [minVer, maxVer] = [minRev.version, maxRev.version];
const label = (
<>
Bisect complete. Check the range{' '}
<a
target="_blank"
rel="noreferrer"
href={`https://github.com/electron/electron/compare/v${minVer}...v${maxVer}`}
>
{minVer}...{maxVer}
</a>
.
</>
);
const message = `Check between versions ${minVer} and ${maxVer}.`;
appState.pushOutput(`[BISECT] Complete: ${minVer}...${maxVer}`);
appState.pushOutput(`[BISECT] Complete: ${message}`);
appState.setGenericDialogOptions({
type: GenericDialogType.success,
label,
cancel: undefined,
label: `Bisect complete. ${message}`,
cancel: undefined
});
appState.isGenericDialogShowing = true;
} else {
@@ -63,8 +51,7 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
public render() {
const { appState } = this.props;
if (!!appState.Bisector) {
const isDownloading =
appState.currentElectronVersion.state === VersionState.downloading;
const isDownloading = appState.currentElectronVersion.state === VersionState.downloading;
return (
<>
<Button
@@ -77,7 +64,10 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
onClick={() => this.continueBisect(false)}
disabled={isDownloading}
/>
<Button icon={'cross'} onClick={this.terminateBisect}>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
@@ -85,8 +75,8 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
} else {
return (
<Button
icon="git-branch"
text="Bisect"
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
+16 -22
View File
@@ -1,11 +1,4 @@
import {
Button,
Menu,
MenuDivider,
MenuItem,
Popover,
Position,
} from '@blueprintjs/core';
import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -29,10 +22,7 @@ export interface EditorDropdownProps {
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
*/
@observer
export class EditorDropdown extends React.Component<
EditorDropdownProps,
EditorDropdownState
> {
export class EditorDropdown extends React.Component<EditorDropdownProps, EditorDropdownState> {
constructor(props: EditorDropdownProps) {
super(props);
@@ -43,7 +33,7 @@ export class EditorDropdown extends React.Component<
return (
<>
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
<Button icon="applications" text="Editors" />
<Button icon='applications' text='Editors' />
</Popover>
{this.renderDocsDemos()}
</>
@@ -57,8 +47,8 @@ export class EditorDropdown extends React.Component<
return (
<Button
icon="help"
text="Docs & Demos"
icon='help'
text='Docs & Demos'
id={PanelId.docsDemo}
onClick={this.onItemClick}
active={!this.props.appState.closedPanels.docsDemo}
@@ -67,7 +57,11 @@ export class EditorDropdown extends React.Component<
}
public renderMenu() {
return <Menu>{...this.renderMenuItems()}</Menu>;
return (
<Menu>
{...this.renderMenuItems()}
</Menu>
);
}
public renderMenuItems() {
@@ -84,20 +78,20 @@ export class EditorDropdown extends React.Component<
id={id}
onClick={this.onItemClick}
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
/>,
/>
);
}
result.push(
<React.Fragment key={'fragment-reset-layout'}>
<MenuDivider />
<MenuDivider />
<MenuItem
icon="grid-view"
key="reset-layout"
text="Reset Layout"
icon='grid-view'
key='reset-layout'
text='Reset Layout'
onClick={appState.resetEditorLayout}
/>
</React.Fragment>,
</React.Fragment>
);
return result;
@@ -0,0 +1,293 @@
import { Button, ButtonGroup, IToastProps, Menu, MenuItem, Popover, Position, Toaster } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { when } from 'mobx';
import { EditorValues } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../../shared-constants';
import { getOctokit } from '../../utils/octokit';
import { EMPTY_EDITOR_CONTENT } from '../constants';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface PublishButtonProps {
appState: AppState;
}
interface IPublishButtonState {
readonly isUpdating: boolean;
readonly wouldPublish: boolean;
}
/**
* The "publish" button takes care of logging you in.
*
* @export
* @class PublishButton
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
*/
@observer
export class PublishButton extends React.Component<PublishButtonProps, IPublishButtonState> {
public constructor(props: PublishButtonProps) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.publishOrUpdateFiddle = this.publishOrUpdateFiddle.bind(this);
this.setPrivate = this.setPrivate.bind(this);
this.setPublic = this.setPublic.bind(this);
this.state = {
isUpdating: false,
wouldPublish: false,
};
}
private toaster: Toaster;
private refHandlers = {
toaster: (ref: Toaster) => this.toaster = ref,
};
public componentDidMount() {
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
public componentWillUnmount() {
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
/**
* When the user clicks the publish button, we either show the
* authentication dialog or publish right away.
*
* If we're showing the authentication dialog, we wait for it
* to be closed again (or a GitHub token to show up) before
* we publish
*
* @returns {Promise<void>}
* @memberof PublishButton
*/
public async handleClick(): Promise<void> {
const { appState } = this.props;
if (!appState.gitHubToken) {
appState.toggleAuthDialog();
}
// Wait for the dialog to be closed again
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
if (appState.gitHubToken) {
return this.publishOrUpdateFiddle();
}
}
/**
* Connect with GitHub, publish the current Fiddle as a gist,
* and update all related properties in the app state.
*/
public async publishOrUpdateFiddle(): Promise<void> {
const { appState } = this.props;
const { wouldPublish } = this.state;
appState.isPublishing = true;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic, gistId } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getEditorValues(options);
if (gistId && !wouldPublish) {
this.setState({
isUpdating: true
});
const gist = await octo.gists.update({
gist_id: appState.gistId,
files: this.gistFilesList(values) as any,
});
console.log('Updating: Updating done', { gist });
this.renderToast({ message: 'Successfully updated gist!' });
this.setState({
isUpdating: false
});
} else {
try {
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description: 'Electron Fiddle Gist',
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
});
appState.gistId = gist.data.id;
console.log(`Publish Button: Publishing done`, { gist });
this.renderToast({ message: 'Publishing done successfully!' });
} catch (error) {
console.warn(`Could not publish gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
}
appState.isPublishing = false;
}
/**
* Publish fiddles as private.
*
* @memberof PublishButton
*/
public setPrivate() {
this.setPrivacy(false);
}
/**
* Publish fiddles as public.
*
* @memberof PublishButton
*/
public setPublic() {
this.setPrivacy(true);
}
public render() {
const { isPublishing, gistId } = this.props.appState;
const { isUpdating, wouldPublish } = this.state;
const getTextForButton = gistId && !wouldPublish
? 'Update'
: isUpdating
? 'Updating...'
: isPublishing
? 'Publishing...'
: 'Publish';
return (
<>
<fieldset disabled={isPublishing}>
<ButtonGroup className='button-publish'>
{this.renderPrivaryMenu()}
<Button
onClick={this.handleClick}
loading={isPublishing}
icon='upload'
text={getTextForButton}
/>
{this.renderMaybePublishMenu()}
</ButtonGroup>
</fieldset>
<Toaster position={Position.BOTTOM_RIGHT} ref={this.refHandlers.toaster} />
</>
);
}
private renderMaybePublishMenu = () => {
const { gistId } = this.props.appState;
const { wouldPublish } = this.state;
if (!gistId) {
return null;
}
const menu = (
<Menu>
<MenuItem
text='Publish'
active={wouldPublish}
onClick={() => this.setWouldPublish(true)}
/>
<MenuItem
text='Update'
active={!wouldPublish}
onClick={() => this.setWouldPublish(false)}
/>
</Menu>
);
return (
<Popover
content={menu}
position={Position.BOTTOM}
>
<Button
icon='wrench'
/>
</Popover>
);
}
private renderPrivaryMenu = () => {
const { gitHubPublishAsPublic, gistId } = this.props.appState;
if (gistId) {
return null;
}
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
<Menu>
<MenuItem
text='Private'
icon='lock'
active={!gitHubPublishAsPublic}
onClick={this.setPrivate}
/>
<MenuItem
text='Public'
icon='unlock'
active={gitHubPublishAsPublic}
onClick={this.setPublic}
/>
</Menu>
);
return (
<Popover
content={privacyMenu}
position={Position.BOTTOM}
>
<Button
icon={privacyIcon}
/>
</Popover>
);
}
private setWouldPublish = (wouldPublish: boolean) => {
this.setState({
wouldPublish,
});
}
private setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
}
private renderToast = (toast: IToastProps) => {
this.toaster.show(toast);
}
private gistFilesList = (values: EditorValues) => {
return {
[INDEX_HTML_NAME]: {
content: values.html || EMPTY_EDITOR_CONTENT.html,
},
[MAIN_JS_NAME]: {
content: values.main || EMPTY_EDITOR_CONTENT.js,
},
[RENDERER_JS_NAME]: {
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
},
[PRELOAD_JS_NAME]: {
content: values.preload || EMPTY_EDITOR_CONTENT.js,
},
[STYLES_CSS_NAME]: {
content: values.css || EMPTY_EDITOR_CONTENT.css,
},
};
}
}
+6 -5
View File
@@ -5,6 +5,9 @@ import * as React from 'react';
import { VersionState } from '../../interfaces';
import { AppState } from '../state';
export interface RunnerState {
}
export interface RunnerProps {
appState: AppState;
}
@@ -14,10 +17,10 @@ export interface RunnerProps {
* with Electron. It also renders the button that does so.
*
* @class Runner
* @extends {React.Component<RunnerProps>}
* @extends {React.Component<RunnerProps, RunnerState>}
*/
@observer
export class Runner extends React.Component<RunnerProps> {
export class Runner extends React.Component<RunnerProps, RunnerState> {
public render() {
const { isRunning, currentElectronVersion } = this.props.appState;
@@ -26,9 +29,7 @@ export class Runner extends React.Component<RunnerProps> {
if (state === VersionState.downloading) {
props.text = 'Downloading';
props.icon = (
<Spinner size={16} value={currentElectronVersion?.downloadProgress} />
);
props.icon = <Spinner size={16} value={currentElectronVersion?.downloadProgress} />;
} else if (state === VersionState.unzipping) {
props.text = 'Unzipping';
props.icon = <Spinner size={16} />;
+16 -12
View File
@@ -6,7 +6,7 @@ import { AppState } from '../state';
import { AddressBar } from './commands-address-bar';
import { BisectHandler } from './commands-bisect';
import { EditorDropdown } from './commands-editors';
import { GistActionButton } from './commands-action-button';
import { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
import { VersionChooser } from './commands-version-chooser';
@@ -19,10 +19,10 @@ export interface CommandsProps {
* all the things
*
* @class Commands
* @extends {React.Component<CommandsProps>}
* @extends {React.Component<CommandsProps, {}>}
*/
@observer
export class Commands extends React.Component<CommandsProps> {
export class Commands extends React.Component<CommandsProps, {}> {
constructor(props: CommandsProps) {
super(props);
}
@@ -32,22 +32,26 @@ export class Commands extends React.Component<CommandsProps> {
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className="commands">
<div className='commands'>
<div>
<ControlGroup fill={true} vertical={false}>
<VersionChooser appState={appState} />
<Runner appState={appState} />
</ControlGroup>
{isBisectCommandShowing && (
<ControlGroup fill={true} vertical={false}>
<BisectHandler appState={appState} />
</ControlGroup>
)}
{
// tslint:disable-next-line jsx-no-multiline-js
isBisectCommandShowing &&
(
<ControlGroup fill={true} vertical={false}>
<BisectHandler appState={appState} />
</ControlGroup>
)
}
<ControlGroup fill={true} vertical={false}>
<Button
active={appState.isConsoleShowing}
icon="console"
text="Console"
icon='console'
text='Console'
onClick={appState.toggleConsole}
/>
<EditorDropdown appState={appState} />
@@ -55,7 +59,7 @@ export class Commands extends React.Component<CommandsProps> {
</div>
<div>
<AddressBar appState={appState} />
<GistActionButton appState={appState} />
<PublishButton appState={appState} />
</div>
</div>
);
+37 -41
View File
@@ -26,10 +26,7 @@ export interface AddThemeDialogState {
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
*/
@observer
export class AddThemeDialog extends React.Component<
AddThemeDialogProps,
AddThemeDialogState
> {
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
public resetState = { file: undefined };
constructor(props: AddThemeDialogProps) {
@@ -61,25 +58,20 @@ export class AddThemeDialog extends React.Component<
*/
public async onSubmit(): Promise<void> {
const { file } = this.state;
const defaultTheme = !!this.props.appState.theme
? await getTheme(this.props.appState.theme)
: defaultDark;
const defaultTheme = !!this.props.appState.theme ? await getTheme(this.props.appState.theme) : defaultDark;
if (!file) return;
try {
const editor = fsType.readJSONSync(file.path);
if (!editor.base && !editor.rules)
throw Error('File does not match specifications'); // has to have these attributes
defaultTheme.editor = editor as Partial<
MonacoType.editor.IStandaloneThemeData
>;
if (!editor.base && !editor.rules) throw Error('File does not match specifications'); // has to have these attributes
defaultTheme.editor = editor as Partial<MonacoType.editor.IStandaloneThemeData>;
const newTheme = defaultTheme;
const name = editor.name ? editor.name : file.name;
await this.createNewThemeFromMonaco(name, newTheme);
} catch (error) {
this.props.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Error: ${error}, please pick a different file.`,
label: `Error: ${error}, please pick a different file.`
});
this.props.appState.isGenericDialogShowing = true;
return;
@@ -89,39 +81,40 @@ export class AddThemeDialog extends React.Component<
return;
}
public async createNewThemeFromMonaco(
name: string,
newTheme: LoadedFiddleTheme,
): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
await fsType.outputJSON(
themePath,
{
await fsType.outputJSON(themePath, {
...newTheme,
name,
},
{ spaces: 2 },
);
}, {spaces: 2});
this.props.appState.setTheme(themePath);
shell.showItemInFolder(themePath);
return true;
this.props.appState.setTheme(themePath);
shell.showItemInFolder(themePath);
return true;
}
get buttons() {
const canSubmit = !!this.state.file;
return [
<Button
icon="add"
key="submit"
disabled={!canSubmit}
onClick={this.onSubmit}
text="Add"
/>,
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
@@ -140,10 +133,10 @@ export class AddThemeDialog extends React.Component<
<Dialog
isOpen={isThemeDialogShowing}
onClose={this.onClose}
title="Add theme"
className="dialog-add-version"
title='Add theme'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
inputProps={inputProps as any}
@@ -151,8 +144,10 @@ export class AddThemeDialog extends React.Component<
/>
<br />
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
@@ -165,4 +160,5 @@ export class AddThemeDialog extends React.Component<
this.setState(this.resetState);
return;
}
}
+44 -43
View File
@@ -1,11 +1,5 @@
import {
Button,
Callout,
Dialog,
FileInput,
InputGroup,
Intent,
} from '@blueprintjs/core';
import { Button, Callout, Dialog, FileInput, InputGroup, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as path from 'path';
import * as React from 'react';
@@ -36,29 +30,23 @@ export interface AddVersionDialogState {
* @extends {React.Component<AddVersionDialogProps, AddVersionDialogState>}
*/
@observer
export class AddVersionDialog extends React.Component<
AddVersionDialogProps,
AddVersionDialogState
> {
export class AddVersionDialog extends React.Component<AddVersionDialogProps, AddVersionDialogState> {
constructor(props: AddVersionDialogProps) {
super(props);
this.state = {
isValidVersion: false,
isValidElectron: false,
version: '',
version: ''
};
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeVersion = this.onChangeVersion.bind(this);
ipcRendererManager.on(
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
(_event, [file]) => {
this.setFolderPath(file);
},
);
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
this.setFolderPath(file);
});
}
public componentWillUnmount() {
@@ -71,7 +59,7 @@ export class AddVersionDialog extends React.Component<
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async setFolderPath(folderPath: string) {
const isValidElectron = !!(await getIsDownloaded('custom', folderPath));
const isValidElectron = !!await getIsDownloaded('custom', folderPath);
this.setState({ folderPath, isValidElectron });
}
@@ -96,12 +84,16 @@ export class AddVersionDialog extends React.Component<
if (!folderPath) return;
const name = folderPath.slice(-20).split(path.sep).slice(1).join(path.sep);
const name = folderPath
.slice(-20)
.split(path.sep)
.slice(1)
.join(path.sep);
const toAdd: Version = {
localPath: folderPath,
version,
name,
name
};
this.props.appState.addLocalVersion(toAdd);
@@ -120,14 +112,22 @@ export class AddVersionDialog extends React.Component<
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
return [
<Button
icon="add"
key="submit"
disabled={!canSubmit}
onClick={this.onSubmit}
text="Add"
/>,
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
@@ -137,32 +137,33 @@ export class AddVersionDialog extends React.Component<
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
},
}
};
const { folderPath } = this.state;
const text =
folderPath ||
const text = folderPath ||
`Select the folder containing ${getElectronNameForPlatform()}...`;
return (
<Dialog
isOpen={isAddVersionDialogShowing}
onClose={this.onClose}
title="Add local Electron build"
className="dialog-add-version"
title='Add local Electron build'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<FileInput
id="custom-electron-version"
id='custom-electron-version'
inputProps={inputProps as any}
text={text}
/>
<br />
{this.renderPath()}
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
@@ -193,14 +194,14 @@ export class AddVersionDialog extends React.Component<
return (
<>
<p>
Please specify a version, used for typings and the name. Must be{' '}
<code>semver</code> compliant.
Please specify a version, used for typings and the name.
Must be <code>semver</code> compliant.
</p>
<InputGroup
intent={isValidVersion ? undefined : Intent.DANGER}
value={version}
onChange={this.onChangeVersion}
placeholder="4.0.0"
placeholder='4.0.0'
/>
</>
);
@@ -214,7 +215,7 @@ export class AddVersionDialog extends React.Component<
isValidElectron: false,
isValidVersion: false,
version: '',
folderPath: undefined,
folderPath: undefined
});
}
}
+44 -41
View File
@@ -25,10 +25,7 @@ export interface BisectDialogState {
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
*/
@observer
export class BisectDialog extends React.Component<
BisectDialogProps,
BisectDialogState
> {
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
constructor(props: BisectDialogProps) {
super(props);
@@ -43,7 +40,7 @@ export class BisectDialog extends React.Component<
this.state = {
allVersions: this.props.appState.versionsToShow,
startIndex: 10,
endIndex: 0,
endIndex: 0
};
}
@@ -68,7 +65,9 @@ export class BisectDialog extends React.Component<
return;
}
const bisectRange = allVersions.slice(endIndex, startIndex + 1).reverse();
const bisectRange = allVersions
.slice(endIndex, startIndex + 1)
.reverse();
appState.Bisector = new Bisector(bisectRange);
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
@@ -102,14 +101,22 @@ export class BisectDialog extends React.Component<
*/
get buttons() {
return [
<Button
icon="play"
key="submit"
disabled={!this.canSubmit}
onClick={this.onSubmit}
text="Begin"
/>,
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
(
<Button
icon='play'
key='submit'
disabled={!this.canSubmit}
onClick={this.onSubmit}
text='Begin'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
@@ -118,24 +125,25 @@ export class BisectDialog extends React.Component<
*/
get help() {
let moreHelp = (
<Button icon="help" text="Show help" onClick={this.showHelp} />
<Button
icon='help'
text='Show help'
onClick={this.showHelp}
/>
);
if (this.state.showHelp) {
moreHelp = (
<>
<p>
First, write a fiddle that reproduces a bug or an issue. Then,
select the earliest version to start your search with. Typically,
that&apos;s the &quot;last known good&quot; version that did not
have the bug. Then, select that latest version to end the search
with, usually the &quot;first known bad&quot; version.
First, write a fiddle that reproduces a bug or an issue. Then, select the earliest version to
start your search with. Typically, that's the "last known good" version that did not have the bug.
Then, select that latest version to end the search with, usually the "first known bad" version.
</p>
<p>
Once you begin your bisect, Fiddle will run your fiddle with a
number of Electron versions, closing in on the version that
introduced the bug. Once completed, you will know which Electron
version introduced your issue.
Once you begin your bisect, Fiddle will run your fiddle with a number of Electron versions, closing
in on the version that introduced the bug. Once completed, you will know which Electron version
introduced your issue.
</p>
</>
);
@@ -144,16 +152,9 @@ export class BisectDialog extends React.Component<
return (
<Callout style={{ marginTop: 0, marginBottom: '1rem' }}>
<p>
A &quot;bisect&quot; is a popular method{' '}
<a
href="https://git-scm.com/docs/git-bisect"
target="_blank"
rel="noreferrer"
>
borrowed from <code>git</code>
</a>{' '}
for learning which version of Electron introduced a bug. This tool
helps you perform a bisect.
A "bisect" is a popular method <a href='https://git-scm.com/docs/git-bisect' target='_blank'>
borrowed from <code>git</code></a> for learning which version of Electron introduced a bug. This
tool helps you perform a bisect.
</p>
{moreHelp}
</Callout>
@@ -168,13 +169,13 @@ export class BisectDialog extends React.Component<
<Dialog
isOpen={isBisectDialogShowing}
onClose={this.onClose}
title="Start a bisect session"
className="dialog-add-version"
title='Start a bisect session'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
{this.help}
<Label>
Earliest Version (Last &quot;known good&quot; version)
Earliest Version (Last "known good" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[startIndex]}
@@ -185,7 +186,7 @@ export class BisectDialog extends React.Component<
</ButtonGroup>
</Label>
<Label>
Latest Version (First &quot;known bad&quot; version)
Latest Version (First "known bad" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[endIndex]}
@@ -196,8 +197,10 @@ export class BisectDialog extends React.Component<
</ButtonGroup>
</Label>
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
+8 -29
View File
@@ -1,4 +1,5 @@
import { Alert, IconName, InputGroup, Intent } from '@blueprintjs/core';
import { Alert, IconName, Intent} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -9,6 +10,9 @@ export interface GenericDialogProps {
appState: AppState;
}
export interface GenericDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
@@ -17,7 +21,7 @@ export interface GenericDialogProps {
* @extends {React.Component<GenericDialogProps, GenericDialogState>}
*/
@observer
export class GenericDialog extends React.Component<GenericDialogProps> {
export class GenericDialog extends React.Component<GenericDialogProps, GenericDialogState> {
constructor(props: GenericDialogProps) {
super(props);
@@ -25,27 +29,13 @@ export class GenericDialog extends React.Component<GenericDialogProps> {
}
public onClose(result: boolean) {
const input = document.getElementById('input') as HTMLInputElement;
this.props.appState.genericDialogLastInput =
input && input.value !== '' ? input.value : null;
this.props.appState.genericDialogLastResult = result;
this.props.appState.toggleGenericDialog();
}
public render() {
const {
isGenericDialogShowing,
genericDialogOptions,
} = this.props.appState;
const {
type,
ok,
cancel,
label,
wantsInput,
placeholder,
} = genericDialogOptions;
const { isGenericDialogShowing, genericDialogOptions } = this.props.appState;
const {type, ok, cancel, label} = genericDialogOptions;
let intent: Intent;
let icon: IconName;
@@ -67,16 +57,6 @@ export class GenericDialog extends React.Component<GenericDialogProps> {
icon = 'help';
break;
}
let dialogInput;
if (wantsInput) {
dialogInput = placeholder ? (
<InputGroup id="input" placeholder={placeholder} />
) : (
<InputGroup id="input" />
);
}
return (
<Alert
isOpen={isGenericDialogShowing}
@@ -87,7 +67,6 @@ export class GenericDialog extends React.Component<GenericDialogProps> {
intent={intent}
>
<p>{label}</p>
{wantsInput && dialogInput}
</Alert>
);
}
+29 -30
View File
@@ -1,3 +1,4 @@
import { Button, Callout, Dialog, InputGroup, Intent } from '@blueprintjs/core';
import { clipboard, shell } from 'electron';
import { observer } from 'mobx-react';
@@ -16,7 +17,7 @@ export interface TokenDialogState {
error: boolean;
}
const TOKEN_SCOPES = ['gist'].join();
const TOKEN_SCOPES = [ 'gist' ].join();
const TOKEN_DESCRIPTION = encodeURIComponent('Fiddle Gist Token');
const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKEN_SCOPES}&description=${TOKEN_DESCRIPTION}`;
@@ -29,17 +30,14 @@ const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKE
* @extends {React.Component<TokenDialogProps, TokenDialogState>}
*/
@observer
export class TokenDialog extends React.Component<
TokenDialogProps,
TokenDialogState
> {
export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogState> {
constructor(props: TokenDialogProps) {
super(props);
this.state = {
verifying: false,
error: false,
tokenInput: '',
tokenInput: ''
};
this.onSubmitToken = this.onSubmitToken.bind(this);
@@ -93,7 +91,7 @@ export class TokenDialog extends React.Component<
this.setState({
verifying: false,
error: false,
tokenInput: '',
tokenInput: ''
});
}
@@ -136,20 +134,23 @@ export class TokenDialog extends React.Component<
const canSubmit = !!this.state.tokenInput;
return [
<Button
key="done"
disabled={!canSubmit}
onClick={this.onSubmitToken}
loading={this.state.verifying}
text="Done"
icon="log-in"
/>,
<Button
key="cancel"
text="Cancel"
icon="log-out"
onClick={this.onClose}
/>,
(
<Button
key='done'
disabled={!canSubmit}
onClick={this.onSubmitToken}
loading={this.state.verifying}
text='Done'
icon='log-in'
/>
), (
<Button
key='cancel'
text='Cancel'
icon='log-out'
onClick={this.onClose}
/>
)
];
}
@@ -171,15 +172,11 @@ export class TokenDialog extends React.Component<
<Dialog
isOpen={isTokenDialogShowing}
onClose={this.onClose}
title="GitHub Token"
title='GitHub Token'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<p>
Generate a{' '}
<a onClick={this.openGenerateTokenExternal}>
GitHub Personal Access Token
</a>{' '}
and paste it here:
Generate a <a onClick={this.openGenerateTokenExternal}>GitHub Personal Access Token</a> and paste it here:
</p>
{this.state.error ? this.invalidWarning : null}
@@ -190,8 +187,10 @@ export class TokenDialog extends React.Component<
onChange={this.handleChange}
/>
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
+20 -21
View File
@@ -20,7 +20,7 @@ export interface DialogsProps {
* @extends {React.Component<DialogsProps, {}>}
*/
@observer
export class Dialogs extends React.Component<DialogsProps> {
export class Dialogs extends React.Component<DialogsProps, {}> {
public render() {
const { appState } = this.props;
const {
@@ -29,29 +29,28 @@ export class Dialogs extends React.Component<DialogsProps> {
isAddVersionDialogShowing,
isThemeDialogShowing,
isBisectDialogShowing,
isGenericDialogShowing,
isGenericDialogShowing
} = appState;
const maybeToken = isTokenDialogShowing ? (
<TokenDialog key="dialogs" appState={appState} />
) : null;
const maybeSettings = isSettingsShowing ? (
<Settings key="settings" appState={appState} />
) : null;
const maybeAddLocalVersion = isAddVersionDialogShowing ? (
<AddVersionDialog key="add-version-dialog" appState={appState} />
) : null;
const maybeMonaco = isThemeDialogShowing ? (
<AddThemeDialog appState={appState} />
) : null;
const maybeBisect = isBisectDialogShowing ? (
<BisectDialog key="bisect-dialog" appState={appState} />
) : null;
const genericDialog = isGenericDialogShowing ? (
<GenericDialog appState={appState} />
) : null;
const maybeToken = isTokenDialogShowing
? <TokenDialog key='dialogs' appState={appState} />
: null;
const maybeSettings = isSettingsShowing
? <Settings key='settings' appState={appState} />
: null;
const maybeAddLocalVersion = isAddVersionDialogShowing
? <AddVersionDialog key='add-version-dialog' appState={appState} />
: null;
const maybeMonaco = isThemeDialogShowing ? <AddThemeDialog appState={appState} />
: null;
const maybeBisect = isBisectDialogShowing
? <BisectDialog key='bisect-dialog' appState={appState} />
: null;
const genericDialog = isGenericDialogShowing
? <GenericDialog appState={appState} />
: null;
return (
<div key="dialogs" className="dialogs">
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
+6 -9
View File
@@ -15,17 +15,14 @@ export interface EditorProps {
id: EditorId;
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
onChange?: (
value: string,
event: MonacoType.editor.IModelContentChangedEvent,
) => void;
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
setFocused: (id: EditorId) => void;
}
export class Editor extends React.Component<EditorProps> {
public editor: MonacoType.editor.IStandaloneCodeEditor;
public language = 'javascript';
public value = '';
public language: string = 'javascript';
public value: string = '';
private containerRef = React.createRef<HTMLDivElement>();
@@ -90,7 +87,7 @@ export class Editor extends React.Component<EditorProps> {
theme: 'main',
contextmenu: false,
model: null,
...monacoOptions,
...monacoOptions
});
// mark this editor as focused whenever it is
@@ -114,7 +111,7 @@ export class Editor extends React.Component<EditorProps> {
}
public render() {
return <div className="editorContainer" ref={this.containerRef} />;
return <div className='editorContainer' ref={this.containerRef} />;
}
/**
@@ -128,7 +125,7 @@ export class Editor extends React.Component<EditorProps> {
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2,
tabSize: 2
});
this.editor.setModel(model);
@@ -6,10 +6,10 @@ import { EditorId } from '../../interfaces';
import { AppState } from '../state';
export function renderNonIdealState(appState: AppState) {
const allEditors = [EditorId.html, EditorId.main, EditorId.renderer];
const allEditors = [ EditorId.html, EditorId.main, EditorId.renderer ];
const resolveButton = (
<Button
text="Open all editors"
text='Open all editors'
onClick={() => appState.setVisibleMosaics(allEditors)}
/>
);
@@ -17,7 +17,7 @@ export function renderNonIdealState(appState: AppState) {
return (
<NonIdealState
action={resolveButton}
icon="applications"
icon='applications'
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
/>
);
@@ -1,11 +1,7 @@
import * as React from 'react';
import { Button } from '@blueprintjs/core';
import {
MosaicContext,
MosaicRootActions,
MosaicWindowContext,
} from 'react-mosaic-component';
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
import { DocsDemoPage, MosaicId } from '../../interfaces';
import { AppState } from '../state';
@@ -15,9 +11,7 @@ export interface ToolbarButtonProps {
id: MosaicId;
}
export abstract class ToolbarButton extends React.PureComponent<
ToolbarButtonProps
> {
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
public static contextType = MosaicWindowContext;
public context: MosaicWindowContext;
@@ -36,9 +30,7 @@ export abstract class ToolbarButton extends React.PureComponent<
/**
* Create a button that performs the actual action
*/
public abstract createButton(
_mosaicActions: MosaicRootActions<any>,
): React.ReactNode;
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
}
export class MaximizeButton extends ToolbarButton {
@@ -50,7 +42,13 @@ export class MaximizeButton extends ToolbarButton {
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
};
return <Button icon="maximize" className="bp3-small" onClick={onClick} />;
return (
<Button
icon='maximize'
className='bp3-small'
onClick={onClick}
/>
);
}
}
@@ -59,10 +57,15 @@ export class RemoveButton extends ToolbarButton {
* Create a button that can remove this panel
*/
public createButton(_mosaicActions: MosaicRootActions<any>) {
const onClick = () =>
this.props.appState.hideAndBackupMosaic(this.props.id);
const onClick = () => this.props.appState.hideAndBackupMosaic(this.props.id);
return <Button icon="cross" className="bp3-small" onClick={onClick} />;
return (
<Button
icon='cross'
className='bp3-small'
onClick={onClick}
/>
);
}
}
@@ -71,15 +74,14 @@ export class DocsDemoGoHomeButton extends ToolbarButton {
* Create a button that can remove this panel
*/
public createButton(_mosaicActions: MosaicRootActions<any>) {
const onClick = () =>
(this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT);
const onClick = () => this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
return (
<Button
icon="home"
className="bp3-small"
icon='home'
className='bp3-small'
onClick={onClick}
text="Overview"
text='Overview'
/>
);
}
+43 -75
View File
@@ -2,13 +2,7 @@ import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as React from 'react';
import {
Mosaic,
MosaicBranch,
MosaicNode,
MosaicWindow,
MosaicWindowProps,
} from 'react-mosaic-component';
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
import { EditorId, MosaicId, PanelId } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
@@ -23,18 +17,14 @@ import { AppState } from '../state';
import { activateTheme } from '../themes';
import { Editor } from './editor';
import { renderNonIdealState } from './editors-non-ideal-state';
import {
DocsDemoGoHomeButton,
MaximizeButton,
RemoveButton,
} from './editors-toolbar-button';
import { DocsDemoGoHomeButton, MaximizeButton, RemoveButton } from './editors-toolbar-button';
import { ShowMe } from './show-me';
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
minimap: {
enabled: false,
enabled: false
},
wordWrap: 'on',
wordWrap: 'on'
};
export const TITLE_MAP: Record<MosaicId, string> = {
@@ -63,7 +53,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
// the editor layout. That method is itself debounced.
public disposeLayoutAutorun = reaction(
() => this.props.appState.mosaicArrangement,
() => updateEditorLayout(),
() => updateEditorLayout()
);
constructor(props: EditorsProps) {
@@ -84,41 +74,22 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
* @memberof Editors
*/
public async componentDidMount() {
ipcRendererManager.on(
IpcEvents.MONACO_EXECUTE_COMMAND,
(_event, cmd: string) => {
this.executeCommand(cmd);
},
);
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
this.executeCommand(cmd);
});
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
const { version } = this.props.appState;
await window.ElectronFiddle.app.replaceFiddle(
{
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
},
{},
);
await window.ElectronFiddle.app.replaceFiddle({
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
}, {});
});
ipcRendererManager.on(
IpcEvents.MONACO_TOGGLE_OPTION,
(_event, cmd: string) => {
this.toggleEditorOption(cmd);
},
);
ipcRendererManager.on(IpcEvents.SELECT_ALL_IN_EDITOR, (_event) => {
// programmatically fetch all editor contents and set as selection
const editor = getFocusedEditor();
const range = editor?.getModel()?.getFullModelRange();
if (!!range) {
editor?.setSelection(range);
}
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
this.toggleEditorOption(cmd);
});
this.setState({ isMounted: true });
@@ -132,7 +103,6 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_EXECUTE_COMMAND);
ipcRendererManager.removeAllListeners(IpcEvents.FS_NEW_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_TOGGLE_OPTION);
ipcRendererManager.removeAllListeners(IpcEvents.SELECT_ALL_IN_EDITOR);
}
/**
@@ -147,9 +117,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
if (editor) {
const command = editor.getAction(commandId);
console.log(
`Editors: Trying to run ${command.id}. Supported: ${command.isSupported}`,
);
console.log(`Editors: Trying to run ${command.id}. Supported: ${command.isSupported}`);
if (command && command.isSupported()) {
command.run();
@@ -169,14 +137,15 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
setAtPath(path, newOptions, toggleMonaco(currentSetting));
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null =
window.ElectronFiddle.editors[key];
Object.keys(window.ElectronFiddle.editors)
.forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null
= window.ElectronFiddle.editors[key];
if (editor) {
editor.updateOptions(newOptions);
}
});
if (editor) {
editor.updateOptions(newOptions);
}
});
this.setState({ monacoOptions: newOptions });
@@ -196,35 +165,36 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
* @returns {JSX.Element}
*/
public renderToolbar(
{ title }: MosaicWindowProps<MosaicId>,
id: MosaicId,
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
): JSX.Element {
const { appState } = this.props;
const docsDemoGoHomeMaybe =
id === PanelId.docsDemo ? (
<DocsDemoGoHomeButton id={id} appState={appState} />
) : null;
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
? <DocsDemoGoHomeButton id={id} appState={appState} />
: null;
// only show toolbar controls if we have more than 1 visible editor
// Mosaic arrangement is type string if 1 editor, object otherwise
const toolbarControlsMaybe = typeof appState.mosaicArrangement !==
'string' && (
<>
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
</>
);
const toolbarControlsMaybe =
(typeof appState.mosaicArrangement !== 'string') &&
(
<>
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
</>
);
return (
<div>
{/* Left */}
<div>
<h5>{title}</h5>
<h5>
{title}
</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className="mosaic-controls">
<div className='mosaic-controls'>
{docsDemoGoHomeMaybe}
{toolbarControlsMaybe}
</div>
@@ -250,9 +220,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
className={id}
path={path}
title={TITLE_MAP[id]}
renderToolbar={(props: MosaicWindowProps<MosaicId>) =>
this.renderToolbar(props, id)
}
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
>
{content}
</MosaicWindow>
@@ -326,7 +294,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
public async loadMonaco() {
const { app } = window.ElectronFiddle;
const loader = require('monaco-loader');
const monaco = app.monaco || (await loader());
const monaco = app.monaco || await loader();
if (!app.monaco) {
app.monaco = monaco;
@@ -335,7 +303,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
if (!this.state || !this.state.isMounted) {
this.setState({
monaco,
monacoOptions: defaultMonacoOptions,
monacoOptions: defaultMonacoOptions
});
} else {
this.setState({ monaco });
+4 -4
View File
@@ -13,15 +13,15 @@ export interface HeaderProps {
* Everything above the editors, so buttons and the address bar.
*
* @class Header
* @extends {React.Component<HeaderProps>}
* @extends {React.Component<HeaderProps, HeaderState>}
*/
export class Header extends React.Component<HeaderProps> {
export class Header extends React.Component<HeaderProps, {}> {
public render() {
return (
<>
<ChromeMac appState={this.props.appState} />
<header id="header">
<Commands key="commands" appState={this.props.appState} />
<header id='header'>
<Commands key='commands' appState={this.props.appState} />
</header>
<WelcomeTour appState={this.props.appState} />
</>
@@ -15,13 +15,11 @@ export interface WrapperState {
export type WrapperMosaicId = 'output' | 'editors';
export class OutputEditorsWrapper extends React.Component<
WrapperProps,
WrapperState
> {
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
private MOSAIC_ELEMENTS = {
output: <Output appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />
};
constructor(props: any) {
@@ -32,7 +30,7 @@ export class OutputEditorsWrapper extends React.Component<
first: 'output',
second: 'editors',
splitPercentage: 25,
},
}
};
}
@@ -41,7 +39,7 @@ export class OutputEditorsWrapper extends React.Component<
<>
<Mosaic<WrapperMosaicId>
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
resize={{ minimumPaneSizePercentage: 0 }}
resize={{minimumPaneSizePercentage: 0}}
value={this.state.mosaicArrangement}
onChange={this.onChange}
/>
@@ -56,6 +54,6 @@ export class OutputEditorsWrapper extends React.Component<
this.props.appState.isConsoleShowing = isConsoleShowing;
}
this.setState({ mosaicArrangement: currentNode });
};
this.setState({mosaicArrangement: currentNode});
}
}
+8 -9
View File
@@ -7,6 +7,7 @@ import { OutputEntry } from '../../interfaces';
import { AppState } from '../state';
import { WrapperMosaicId } from './output-editors-wrapper';
export interface CommandsProps {
appState: AppState;
// Used to keep testing conform
@@ -18,10 +19,10 @@ export interface CommandsProps {
* whenever a Fiddle is launched in Electron.
*
* @class Output
* @extends {React.Component<CommandsProps>}
* @extends {React.Component<CommandsProps, {}>}
*/
@observer
export class Output extends React.Component<CommandsProps> {
export class Output extends React.Component<CommandsProps, {}> {
public static contextType = MosaicContext;
public context: MosaicContext<WrapperMosaicId>;
private outputRef = React.createRef<HTMLDivElement>();
@@ -33,6 +34,7 @@ export class Output extends React.Component<CommandsProps> {
this.renderEntry = this.renderEntry.bind(this);
}
public componentDidMount() {
autorun(() => {
const { isConsoleShowing } = this.props.appState;
@@ -81,16 +83,13 @@ export class Output extends React.Component<CommandsProps> {
*/
public renderEntry(entry: OutputEntry, index: number): Array<JSX.Element> {
const ts = this.renderTimestamp(entry.timestamp);
const timestamp = <span className="timestamp">{ts}</span>;
const timestamp = <span className='timestamp'>{ts}</span>;
const lines = entry.text.split(/\r?\n/);
const style: React.CSSProperties = entry.isNotPre
? { whiteSpace: 'initial' }
: {};
const style: React.CSSProperties = entry.isNotPre ? { whiteSpace: 'initial' } : {};
return lines.map((text, lineIndex) => (
<p style={style} key={`${entry.timestamp}--${index}--${lineIndex}`}>
{timestamp}
{text}
{timestamp}{text}
</p>
));
}
@@ -108,7 +107,7 @@ export class Output extends React.Component<CommandsProps> {
.map(this.renderEntry);
return (
<div className="output" ref={this.outputRef}>
<div className='output' ref={this.outputRef}>
{lines}
</div>
);
+17 -24
View File
@@ -30,15 +30,12 @@ export interface Contributor {
* @class CreditsSettings
* @extends {React.Component<CreditsSettingsProps, CreditsSettingsState>}
*/
export class CreditsSettings extends React.Component<
CreditsSettingsProps,
CreditsSettingsState
> {
export class CreditsSettings extends React.Component<CreditsSettingsProps, CreditsSettingsState> {
constructor(props: CreditsSettingsProps) {
super(props);
this.state = {
contributors: [],
contributors: []
};
this.getContributors();
@@ -53,25 +50,22 @@ export class CreditsSettings extends React.Component<
const { contributors } = this.state;
return contributors.map(({ name, avatar, url, login, location, bio }) => {
const maybeLocation = location ? (
<p className="location">📍 {location}</p>
) : null;
const maybeBio = bio ? <small className="bio">{bio}</small> : null;
const maybeLocation = location
? <p className='location'>📍 {location}</p>
: null;
const maybeBio = bio
? <small className='bio'>{bio}</small>
: null;
const style: React.CSSProperties = {
backgroundImage: `url(${avatar})`,
backgroundImage: `url(${avatar})`
};
const onClick = () => shell.openExternal(url);
return (
<Card
interactive={true}
key={login}
className="contributor"
onClick={onClick}
>
<div className="avatar" style={style} />
<div className="details">
<h5 className="name">{name || login}</h5>
<Card interactive={true} key={login} className='contributor' onClick={onClick}>
<div className='avatar' style={style} />
<div className='details'>
<h5 className='name'>{name || login}</h5>
{maybeLocation}
{maybeBio}
</div>
@@ -90,17 +84,16 @@ export class CreditsSettings extends React.Component<
would like to thank those who helped to make Electron Fiddle:
</Callout>
<br />
<div className="contributors">{this.renderContributors()}</div>
<div className='contributors'>
{this.renderContributors()}
</div>
</div>
);
}
public async getContributors() {
try {
const contributorsFile = path.join(
__dirname,
'../../static/contributors.json',
);
const contributorsFile = path.join(__dirname, '../../static/contributors.json');
const contributors = await fs.readJSON(contributorsFile);
this.setState({ contributors });
} catch (error) {
+71 -54
View File
@@ -8,7 +8,7 @@ import {
IButtonProps,
Icon,
IconName,
Tooltip,
Tooltip
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -35,10 +35,7 @@ export interface ElectronSettingsState {
* @extends {React.Component<ElectronSettingsProps, {}>}
*/
@observer
export class ElectronSettings extends React.Component<
ElectronSettingsProps,
ElectronSettingsState
> {
export class ElectronSettings extends React.Component<ElectronSettingsProps, ElectronSettingsState> {
constructor(props: ElectronSettingsProps) {
super(props);
@@ -51,7 +48,7 @@ export class ElectronSettings extends React.Component<
this.state = {
isDownloadingAll: false,
isDeletingAll: false,
isDeletingAll: false
};
}
@@ -64,7 +61,9 @@ export class ElectronSettings extends React.Component<
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleStateChange(event: React.FormEvent<HTMLInputElement>) {
public handleStateChange(
event: React.FormEvent<HTMLInputElement>
) {
const { id, checked } = event.currentTarget;
const { appState } = this.props;
@@ -80,7 +79,9 @@ export class ElectronSettings extends React.Component<
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleChannelChange(event: React.FormEvent<HTMLInputElement>) {
public handleChannelChange(
event: React.FormEvent<HTMLInputElement>
) {
const { id, checked } = event.currentTarget;
const { appState } = this.props;
@@ -137,7 +138,7 @@ export class ElectronSettings extends React.Component<
public render() {
return (
<div className="settings-electron">
<div className='settings-electron'>
<h2>Electron Settings</h2>
<Callout>
{this.renderVersionChannelOptions()}
@@ -169,25 +170,25 @@ export class ElectronSettings extends React.Component<
disabled={isUpdatingElectronVersions}
onClick={this.handleDownloadClick}
loading={isUpdatingElectronVersions}
icon="numbered-list"
text="Update Electron Release List"
icon='numbered-list'
text='Update Electron Release List'
/>
<Button
disabled={isWorking}
icon="download"
icon='download'
onClick={this.handleDownloadAll}
text="Download All Versions"
text='Download All Versions'
/>
<Button
disabled={isWorking}
icon="trash"
icon='trash'
onClick={this.handleDeleteAll}
text="Delete All Downloads"
text='Delete All Downloads'
/>
<Button
icon="document-open"
icon='document-open'
onClick={this.handleAddVersion}
text="Add Local Electron Build"
text='Add Local Electron Build'
/>
</ButtonGroup>
);
@@ -207,12 +208,18 @@ export class ElectronSettings extends React.Component<
};
return (
<FormGroup label="Include Electron versions that are:">
<Tooltip content="Always enabled" position="bottom" intent="primary">
<FormGroup
label='Include Electron versions that are:'
>
<Tooltip
content='Always enabled'
position='bottom'
intent='primary'
>
<Checkbox
checked={getIsChecked(VersionState.ready)}
label="Ready"
id="ready"
label='Ready'
id='ready'
onChange={this.handleStateChange}
inline={true}
disabled={true}
@@ -220,15 +227,15 @@ export class ElectronSettings extends React.Component<
</Tooltip>
<Checkbox
checked={getIsChecked(VersionState.downloading)}
label="Downloading"
id="downloading"
label='Downloading'
id='downloading'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(VersionState.unknown)}
label="Not Downloaded"
id="unknown"
label='Not Downloaded'
id='unknown'
onChange={this.handleStateChange}
inline={true}
/>
@@ -249,9 +256,7 @@ export class ElectronSettings extends React.Component<
return appState.channelsToShow.includes(channel);
};
const getIsCurrentVersionReleaseChannel = (
channel: ElectronReleaseChannel,
) => {
const getIsCurrentVersionReleaseChannel = (channel: ElectronReleaseChannel) => {
return getReleaseChannel(appState.version) === channel;
};
@@ -259,29 +264,34 @@ export class ElectronSettings extends React.Component<
stable: ElectronReleaseChannel.stable,
beta: ElectronReleaseChannel.beta,
nightly: ElectronReleaseChannel.nightly,
unsupported: ElectronReleaseChannel.unsupported,
unsupported: ElectronReleaseChannel.unsupported
};
return (
<FormGroup label="Include Electron versions from these release channels:">
{Object.entries(channels).map(([_, channel]) => (
<Tooltip
content={`Can't disable channel of selected version (${appState.version})`}
disabled={!getIsCurrentVersionReleaseChannel(channel)}
position="bottom"
intent="primary"
key={channel}
>
<Checkbox
checked={getIsChecked(channel)}
label={channel}
id={channel}
onChange={this.handleChannelChange}
disabled={getIsCurrentVersionReleaseChannel(channel)}
inline={true}
/>
</Tooltip>
))}
<FormGroup
label='Include Electron versions from these release channels:'
>
{
// tslint:disable-next-line:jsx-no-multiline-js
Object.entries(channels).map(([_, channel]) => (
<Tooltip
content={`Can't disable channel of selected version (${appState.version})`}
disabled={!getIsCurrentVersionReleaseChannel(channel)}
position='bottom'
intent='primary'
key={channel}
>
<Checkbox
checked={getIsChecked(channel)}
label={channel}
id={channel}
onChange={this.handleChannelChange}
disabled={getIsCurrentVersionReleaseChannel(channel)}
inline={true}
/>
</Tooltip>
))
}
</FormGroup>
);
}
@@ -294,15 +304,20 @@ export class ElectronSettings extends React.Component<
*/
private renderTable(): JSX.Element {
return (
<HTMLTable className="electron-versions-table" striped={true}>
<HTMLTable
className='electron-versions-table'
striped={true}
>
<thead>
<tr>
<th>Version</th>
<th>Status</th>
<th className="action">Action</th>
<th className='action'>Action</th>
</tr>
</thead>
<tbody>{this.renderTableRows()}</tbody>
<tbody>
{this.renderTableRows()}
</tbody>
</HTMLTable>
);
}
@@ -331,7 +346,7 @@ export class ElectronSettings extends React.Component<
<tr key={item.version}>
<td>{item.version}</td>
<td>{this.renderHumanState(item)}</td>
<td className="action">{this.renderAction(key, item)}</td>
<td className='action'>{this.renderAction(key, item)}</td>
</tr>
);
});
@@ -376,14 +391,16 @@ export class ElectronSettings extends React.Component<
const { appState } = this.props;
const buttonProps: IButtonProps = {
fill: true,
small: true,
small: true
};
// Already downloaded
if (state === 'ready') {
buttonProps.onClick = () => appState.removeVersion(key);
buttonProps.icon = 'trash';
buttonProps.text = source === VersionSource.local ? 'Remove' : 'Delete';
buttonProps.text = source === VersionSource.local
? 'Remove'
: 'Delete';
} else if (state === 'downloading') {
buttonProps.disabled = true;
buttonProps.loading = true;
+26 -65
View File
@@ -1,15 +1,7 @@
import {
Callout,
Checkbox,
FormGroup,
InputGroup,
Radio,
RadioGroup,
} from '@blueprintjs/core';
import { Callout, Checkbox, FormGroup, InputGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { IPackageManager } from '../npm';
import { AppState } from '../state';
export interface ExecutionSettingsProps {
@@ -23,14 +15,12 @@ export interface ExecutionSettingsProps {
* @extends {React.Component<ExecutionSettingsProps, {}>}
*/
@observer
export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {}> {
constructor(props: ExecutionSettingsProps) {
super(props);
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(
this,
);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
}
@@ -40,7 +30,9 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleDeleteDataChange(event: React.FormEvent<HTMLInputElement>) {
public handleDeleteDataChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isKeepingUserDataDirs = checked;
}
@@ -50,7 +42,9 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleElectronLoggingChange(event: React.FormEvent<HTMLInputElement>) {
public handleElectronLoggingChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isEnablingElectronLogging = checked;
}
@@ -60,7 +54,9 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleExecutionFlagChange(event: React.FormEvent<HTMLInputElement>) {
public handleExecutionFlagChange(
event: React.FormEvent<HTMLInputElement>
) {
const { value } = event.currentTarget;
const flags = value.split('|');
this.props.appState.executionFlags = flags;
@@ -70,7 +66,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
const {
isKeepingUserDataDirs,
isEnablingElectronLogging,
executionFlags = [],
executionFlags = []
} = this.props.appState;
const deleteUserDirLabel = `
@@ -87,15 +83,14 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
<div>
<h2>Execution</h2>
<Callout>
These advanced settings control how Electron Fiddle executes your
fiddles.
These advanced settings control how Electron Fiddle executes your fiddles.
</Callout>
<br />
<Callout>
<FormGroup label={deleteUserDirLabel}>
<Checkbox
checked={isKeepingUserDataDirs}
label="Do not delete user data directories."
label='Do not delete user data directories.'
onChange={this.handleDeleteDataChange}
/>
</FormGroup>
@@ -105,7 +100,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
<FormGroup label={electronLoggingLabel}>
<Checkbox
checked={isEnablingElectronLogging}
label="Enable advanced Electron logging."
label='Enable advanced Electron logging.'
onChange={this.handleElectronLoggingChange}
/>
</FormGroup>
@@ -113,58 +108,24 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
<br />
<Callout>
<FormGroup>
<p>
Electron allows starting the executable with{' '}
<a href="https://www.electronjs.org/docs/api/command-line-switches">
user-provided flags
</a>
, such as &apos;--js-flags=--expose-gc&apos;. Those can be added
here as bar-separated (|) flags to run when you start your
Fiddles.
</p>
<p>
Electron allows starting the executable with <a
href='https://www.electronjs.org/docs/api/command-line-switches'
>
user-provided flags
</a>
, such as '--js-flags=--expose-gc'. Those can be added here as bar-separated (|)
flags to run when you start your Fiddles.
</p>
<br />
<InputGroup
placeholder="--js-flags=--expose-gc|--lang=es"
placeholder='--js-flags=--expose-gc|--lang=es'
value={executionFlags.join('|')}
onChange={this.handleExecutionFlagChange}
/>
</FormGroup>
</Callout>
<br />
<Callout>
<FormGroup>
<span style={{ marginRight: 4 }}>
Electron Fiddle will install packages on runtime if they are
imported within your fiddle with <code>require</code>. It uses{' '}
<a href="https://www.npmjs.com/" target="_blank" rel="noreferrer">
npm
</a>{' '}
as its package manager by default, but{' '}
<a
href="https://classic.yarnpkg.com/lang/en/"
target="_blank"
rel="noreferrer"
>
Yarn
</a>{' '}
is also available.
</span>
<RadioGroup
onChange={this.handlePMChange}
selectedValue={this.props.appState.packageManager}
inline={true}
>
<Radio label="npm" value="npm" />
<Radio label="yarn" value="yarn" />
</RadioGroup>
</FormGroup>
</Callout>
</div>
);
}
private handlePMChange = (event: React.FormEvent<HTMLInputElement>) => {
this.props.appState.packageManager = event.currentTarget
.value as IPackageManager;
};
}
@@ -22,13 +22,11 @@ const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
* @param {RunnableVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (
query,
{ name },
) => {
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
return name.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
@@ -37,10 +35,7 @@ export const filterItem: ItemPredicate<LoadedFiddleTheme> = (
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (
item,
{ handleClick, modifiers, query },
) => {
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
@@ -52,14 +47,13 @@ export const renderItem: ItemRenderer<LoadedFiddleTheme> = (
text={highlightText(item.name, query)}
key={item.name}
onClick={handleClick}
icon="media"
icon='media'
/>
);
};
export interface AppearanceSettingsProps {
appState: AppState;
toggleHasPopoverOpen: () => void;
}
export interface AppearanceSettingsState {
@@ -75,8 +69,7 @@ export interface AppearanceSettingsState {
*/
@observer
export class AppearanceSettings extends React.Component<
AppearanceSettingsProps,
AppearanceSettingsState
AppearanceSettingsProps, AppearanceSettingsState
> {
public constructor(props: AppearanceSettingsProps) {
super(props);
@@ -86,13 +79,13 @@ export class AppearanceSettings extends React.Component<
this.handleAddTheme = this.handleAddTheme.bind(this);
this.state = {
themes: [],
themes: []
};
getAvailableThemes().then((themes) => {
const { theme } = this.props.appState;
const selectedTheme =
(theme && themes.find(({ file }) => file === theme)) || themes[0];
const selectedTheme = theme &&
themes.find(({ file }) => file === theme) || themes[0];
this.setState({ themes, selectedTheme });
});
@@ -128,20 +121,16 @@ export class AppearanceSettings extends React.Component<
const name = namor.generate({ words: 2, numbers: 0 });
const themePath = path.join(THEMES_PATH, `${name}.json`);
await fs.outputJSON(
themePath,
{
...theme,
name,
file: undefined,
css: undefined,
},
{ spaces: 2 },
);
await fs.outputJSON(themePath, {
...theme,
name,
file: undefined,
css: undefined
}, { spaces: 2 });
shell.showItemInFolder(themePath);
this.setState({ themes: await getAvailableThemes() });
this.setState({themes: await getAvailableThemes()});
return true;
} catch (error) {
@@ -172,54 +161,51 @@ export class AppearanceSettings extends React.Component<
public render() {
const { selectedTheme } = this.state;
const selectedName =
(selectedTheme && selectedTheme.name) || 'Select a theme';
const selectedName = selectedTheme && selectedTheme.name || 'Select a theme';
return (
<div className="settings-appearance">
<div className='settings-appearance'>
<h4>Appearance</h4>
<FormGroup label="Choose your theme" inline={true}>
<FormGroup
label='Choose your theme'
inline={true}
>
<ThemeSelect
filterable={true}
items={this.state.themes}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.handleChange}
popoverProps={{
onClosed: () => this.props.toggleHasPopoverOpen(),
}}
noResults={<MenuItem disabled={true} text="No results." />}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
id="open-theme-selector"
text={selectedName}
icon="tint"
onClick={() => this.props.toggleHasPopoverOpen()}
icon='tint'
/>
</ThemeSelect>
</FormGroup>
<Callout>
<p>
To add themes, add JSON theme files to{' '}
<a id="open-theme-folder" onClick={this.openThemeFolder}>
To add themes, add JSON theme files to <a
id='open-theme-folder'
onClick={this.openThemeFolder}
>
<code>{THEMES_PATH}</code>
</a>
. The easiest way to get started is to clone one of the two existing
</a>. The easiest way to get started is to clone one of the two existing
themes and to add your own colors.
</p>
<p>
Additionally, if you wish to import a Monaco Editor theme, pick your
JSON file and Fiddle will attempt to import it.
Additionally, if you wish to import a Monaco Editor theme, pick your JSON file and Fiddle will attempt to import it.
</p>
<Button
onClick={this.createNewThemeFromCurrent}
text="Create theme from current selection"
icon="duplicate"
text='Create theme from current selection'
icon='duplicate'
/>
<Button
icon="document-open"
icon='document-open'
onClick={this.handleAddTheme}
text="Add a Monaco Editor theme"
text='Add a Monaco Editor theme'
/>
</Callout>
</div>
@@ -232,4 +218,5 @@ export class AppearanceSettings extends React.Component<
public handleAddTheme(): void {
this.props.appState.toggleAddMonacoThemeDialog();
}
}
@@ -12,10 +12,10 @@ export interface ConsoleSettingsProps {
* Settings content to manage console-related preferences.
*
* @class ConsoleSettings
* @extends {React.Component<ConsoleSettingsProps>}
* @extends {React.Component<ConsoleSettingsProps, {}>}
*/
@observer
export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
constructor(props: ConsoleSettingsProps) {
super(props);
@@ -28,7 +28,9 @@ export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleClearOnRunChange(event: React.FormEvent<HTMLInputElement>) {
public handleClearOnRunChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isClearingConsoleOnRun = checked;
}
@@ -47,7 +49,7 @@ export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
<FormGroup label={clearOnRunLabel}>
<Checkbox
checked={isClearingConsoleOnRun}
label="Clear on run."
label='Clear on run.'
onChange={this.handleClearOnRunChange}
/>
</FormGroup>
@@ -15,7 +15,7 @@ export interface GitHubSettingsProps {
* @extends {React.Component<GitHubSettingsProps, {}>}
*/
@observer
export class GitHubSettings extends React.Component<GitHubSettingsProps> {
export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
constructor(props: GitHubSettingsProps) {
super(props);
@@ -31,10 +31,10 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps> {
return (
<Callout>
<p>
Your fiddles can be published as GitHub Gists - that way you can share
your fiddles with the world!
Your fiddles can be published as GitHub Gists -
that way you can share your fiddles with the world!
</p>
<Button onClick={this.signIn} icon="log-in" text="Sign in" />
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
</Callout>
);
}
@@ -51,11 +51,11 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps> {
return (
<Callout>
<p>
Your fiddles can be published as public GitHub Gists. Using the
personal access token you gave us, we logged you into GitHub as{' '}
<code>{gitHubLogin}</code>.
Your fiddles can be published as public GitHub Gists.
Using the personal access token you gave us, we
logged you into GitHub as <code>{gitHubLogin}</code>.
</p>
<Button onClick={signOut} icon="log-out" text="Sign out" />
<Button onClick={signOut} icon='log-out' text='Sign out'/>
</Callout>
);
}
+3 -7
View File
@@ -9,25 +9,21 @@ import { GitHubSettings } from './settings-general-github';
export interface GeneralSettingsProps {
appState: AppState;
toggleHasPopoverOpen: () => void;
}
/**
* Settings content to manage GitHub-related preferences.
*
* @class GitHubSettings
* @extends {React.Component<GeneralSettingsProps>}
* @extends {React.Component<GeneralSettingsProps, {}>}
*/
@observer
export class GeneralSettings extends React.Component<GeneralSettingsProps> {
export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
public render() {
return (
<div>
<h2>General Settings</h2>
<AppearanceSettings
appState={this.props.appState}
toggleHasPopoverOpen={() => this.props.toggleHasPopoverOpen()}
/>
<AppearanceSettings appState={this.props.appState} />
<Divider />
<ConsoleSettings appState={this.props.appState} />
<Divider />
+11 -29
View File
@@ -12,14 +12,14 @@ enum SettingsSections {
General = 'General',
Electron = 'Electron',
Execution = 'Execution',
Credits = 'Credits',
Credits = 'Credits'
}
const settingsSections = [
SettingsSections.General,
SettingsSections.Electron,
SettingsSections.Execution,
SettingsSections.Credits,
SettingsSections.Credits
];
export interface SettingsProps {
@@ -28,7 +28,6 @@ export interface SettingsProps {
export interface SettingsState {
section: SettingsSections;
hasPopoverOpen: boolean;
}
/**
@@ -43,8 +42,7 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
super(props);
this.state = {
section: SettingsSections.General,
hasPopoverOpen: false,
section: SettingsSections.General
};
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
@@ -55,7 +53,7 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.closeSettingsPanel, true);
window.removeEventListener('keyup', this.closeSettingsPanel);
}
/**
@@ -69,12 +67,7 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
const { appState } = this.props;
if (section === SettingsSections.General) {
return (
<GeneralSettings
appState={appState}
toggleHasPopoverOpen={() => this.toggleHasPopoverOpen()}
/>
);
return <GeneralSettings appState={appState} />;
}
if (section === SettingsSections.Electron) {
@@ -124,13 +117,13 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
if (!isSettingsShowing) return null;
return (
<div className="settings">
<div className="settings-menu">
<div className='settings'>
<div className='settings-menu'>
<ul>{this.renderOptions()}</ul>
</div>
<div className="settings-content">
<div className="settings-close" onClick={appState.toggleSettings}>
<Icon icon="cross" />
<div className='settings-content'>
<div className='settings-close' onClick={appState.toggleSettings}>
<Icon icon='cross' />
</div>
{this.renderContent()}
</div>
@@ -158,22 +151,11 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
/**
* Trigger closing of the settings panel upon Esc
* If hasPopoverOpen is set to true, settings will not close as only the popover should close
*/
private closeSettingsPanel(event: KeyboardEvent) {
const { appState } = this.props;
if (event.code === 'Escape' && !this.state.hasPopoverOpen) {
if (event.code === 'Escape') {
appState.isSettingsShowing = false;
}
}
/**
* Toggles whether there is a popover open
*/
public toggleHasPopoverOpen(): void {
this.setState({
...this.state,
hasPopoverOpen: !this.state.hasPopoverOpen,
});
}
}
+4 -5
View File
@@ -13,17 +13,16 @@ export interface ShowMeProps {
* panel.
*
* @class ShowMe
* @extends {React.Component<ShowMeProps>}
* @extends {React.Component<ShowMeProps, ShowMeState>}
*/
@observer
export class ShowMe extends React.Component<ShowMeProps> {
export class ShowMe extends React.Component<ShowMeProps, {}> {
public render() {
const { currentDocsDemoPage: showMeName } = this.props.appState;
const Content =
DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
const Content = DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
return (
<div className="show-me-panel">
<div className='show-me-panel'>
<Content appState={this.props.appState} />
</div>
);
+48 -47
View File
@@ -24,21 +24,18 @@ function ShowHide() {
return (
<>
<p>
This button hides Electron Fiddle right away, showing it again in two
seconds.
</p>
<p>This button hides Electron Fiddle right away, showing it again in two seconds.</p>
<Button
id="show-hide"
icon="eye-off"
text="Hide Electron Fiddle"
id='show-hide'
icon='eye-off'
text='Hide Electron Fiddle'
onClick={onClick}
/>
</>
);
}
export function ShowMeApp(): JSX.Element {
const [secondsLeft, setSeconds] = React.useState(-1);
export function ShowMeApp(_props: any): JSX.Element {
const [ secondsLeft, setSeconds ] = React.useState(-1);
const playFocus = () => {
setSeconds(3);
@@ -50,7 +47,7 @@ export function ShowMeApp(): JSX.Element {
}, 3000);
};
const [paths, setPaths] = React.useState('');
const [ paths, setPaths ] = React.useState('');
const playPaths = () => {
const pathsToQuery = [
'home',
@@ -58,7 +55,7 @@ export function ShowMeApp(): JSX.Element {
'userData',
'temp',
'downloads',
'desktop',
'desktop'
];
let result = '';
@@ -69,71 +66,75 @@ export function ShowMeApp(): JSX.Element {
setPaths(result);
};
const [metrics, setMetrics] = React.useState('');
const [ metrics, setMetrics ] = React.useState('');
const playMetrics = () => {
setMetrics(JSON.stringify(remote.app.getAppMetrics(), undefined, 2));
};
return (
<>
<Icon
icon="help"
iconSize={40}
style={{ float: 'left', margin: '0 10px 0 0' }}
/>
<p className="bp3r-running-text">
The <code>app</code> module controls the app&apos;s application
life-cycle. Most of the events and methods available on this module are
responsible for handling how your interacts with the operating system or
to set application-wide settings.
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
<p className='bp3r-running-text'>
The <code>app</code> module controls the app's application life-cycle. Most of the
events and methods available on this module are responsible for handling how your
interacts with the operating system or to set application-wide settings.
</p>
<h3>API Demos</h3>
{getSubsetOnly('app')}
<Callout title="Hiding, Showing, Focussing" icon="eye-open">
<Callout
title='Hiding, Showing, Focussing'
icon='eye-open'
>
<p>
The app can ask the operating system for window focus. On macOS, it
can additionally request that the app be hidden or shown. Give it a
try: Click on the button below, focus another app, and wait for two
seconds to see Electron Fiddle become the focused app again.
The app can ask the operating system for window focus. On macOS, it can additionally
request that the app be hidden or shown. Give it a try: Click on the button below,
focus another app, and wait for two seconds to see Electron Fiddle become the focused
app again.
</p>
<Button
id="focus"
icon="lightbulb"
text={`Focus Electron Fiddle${
secondsLeft > 0 ? ` in ${secondsLeft}s` : ''
}`}
id='focus'
icon='lightbulb'
text={`Focus Electron Fiddle${secondsLeft > 0 ? ` in ${secondsLeft}s` : ''}`}
onClick={playFocus}
/>
<ShowHide />
</Callout>
<Callout title="Paths" icon="folder-open">
<Callout
title='Paths'
icon='folder-open'
>
<p>
Need to query information about various paths in a cross-platform
manner? Electron can help. The button queries the operating system for
some of them.
Need to query information about various paths in a cross-platform manner? Electron
can help. The button queries the operating system for some of them.
</p>
<Button
id="special-paths"
icon="play"
id='special-paths'
icon='play'
text={`Get special directory paths`}
onClick={playPaths}
/>
<pre id="special-paths-content">{paths}</pre>
<pre id='special-paths-content'>
{paths}
</pre>
</Callout>
<Callout title="Process & Device Information" icon="pulse">
<Callout
title='Process & Device Information'
icon='pulse'
>
<p>
Need to query information about the process or system? The{' '}
<code>app</code> module lets developers query for information about
the running app, hardware, and operating system. A good example are
process metrics.
Need to query information about the process or system? The <code>app</code> module lets
developers query for information about the running app, hardware, and operating system.
A good example are process metrics.
</p>
<Button
id="process-metrics"
icon="play"
id='process-metrics'
icon='play'
text={`Get process metrics`}
onClick={playMetrics}
/>
<pre id="process-metrics-content">{metrics}</pre>
<pre id='process-metrics-content'>
{metrics}
</pre>
</Callout>
{renderMoreDocumentation()}
</>
+11 -15
View File
@@ -14,33 +14,29 @@ export function ShowMeDefault(props: { appState: AppState }): JSX.Element {
<li key={key}>
<Button
minimal={true}
icon="play"
icon='play'
text={DOCS_DEMO_NAMES[key]}
onClick={() =>
(props.appState.currentDocsDemoPage = key as DocsDemoPage)
}
onClick={() => (props.appState.currentDocsDemoPage = key as DocsDemoPage)}
/>
</li>
);
});
});
return (
<>
<Icon
icon="help"
iconSize={40}
style={{ float: 'left', margin: '0 10px 0 0' }}
/>
<p className="bp3r-running-text">
This panel offers useful information about Electron APIs  and easy way
to try some of the methods it offers. Fiddle has an example fiddle for
every module available in Electron.
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
<p className='bp3r-running-text'>
This panel offers useful information about Electron APIs  and
easy way to try some of the methods it offers. Fiddle has an example fiddle
for every module available in Electron.
</p>
<p>
Clicking on any of the modules below will open up a fiddle showcasing
that particular module.
</p>
<ul className="show-me-list">{...showMeMenu}</ul>
<ul className='show-me-list'>
{...showMeMenu}
</ul>
{renderMoreDocumentation()}
</>
);
+2 -2
View File
@@ -5,10 +5,10 @@ import { ShowMeDefault } from './default';
export const DOCS_DEMO_COMPONENTS: Record<DocsDemoPage, any> = {
DEFAULT: ShowMeDefault,
DEMO_APP: ShowMeApp,
DEMO_APP: ShowMeApp
};
export const DOCS_DEMO_NAMES: Record<DocsDemoPage, string> = {
DEFAULT: 'Home',
DEMO_APP: 'App Demos',
DEMO_APP: 'App Demos'
};
@@ -10,7 +10,7 @@ import { shell } from 'electron';
* @returns {JSX.Element}
*/
export function renderMoreDocumentation(
url = 'electronjs.org/docs',
url: string = 'electronjs.org/docs'
): JSX.Element {
const fullUrl = `https://${url}`;
const gitHubUrl = `https://github.com/electron/fiddle`;
@@ -18,21 +18,14 @@ export function renderMoreDocumentation(
return (
<>
<br />
<p className="b3-running-text">
For more documentation, visit{' '}
<a id="open-url" onClick={() => shell.openExternal(fullUrl)}>
{url}
</a>
, where you can find the full documentation for Electron.
<p className='b3-running-text'>
For more documentation, visit <a id='open-url' onClick={() => shell.openExternal(fullUrl)}>{url}</a>, where
you can find the full documentation for Electron.
</p>
<p className="bp3-text-muted">
By the way, Electron Fiddle and the documentation you see here is
entirely open source. If you have ideas on how to improve it, we&apos;d
love to have your contributions! You can find the repository on{' '}
<a id="open-github" onClick={() => shell.openExternal(gitHubUrl)}>
GitHub
</a>
.
<p className='bp3-text-muted'>
By the way, Electron Fiddle and the documentation you see here is entirely open source. If you
have ideas on how to improve it, we'd love to have your contributions! You can find the
repository on <a id='open-github' onClick={() => shell.openExternal(gitHubUrl)}>GitHub</a>.
</p>
</>
);
@@ -17,15 +17,10 @@ export function getSubsetOnly(moduleName: string): JSX.Element {
const { full, short } = getDocsUrlForModule(moduleName);
return (
<p className="bp3-running-text">
The following demos display only a subset of what the{' '}
<code>{moduleName}</code>
<p className='bp3-running-text'>
The following demos display only a subset of what the <code>{moduleName}</code>
module is capable of. If you want to see its full abilities, check out the
documentation on{' '}
<a id="open-url" onClick={() => shell.openExternal(full)}>
{short}
</a>
.
documentation on <a id='open-url' onClick={() => shell.openExternal(full)}>{short}</a>.
</p>
);
}
+74 -76
View File
@@ -28,17 +28,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Electron Fiddle allows you to build little experiments and mini-apps
with Electron. Each Fiddle has at least three of these files: A main
script, a renderer script, a preload script, and an HTML file.
Electron Fiddle allows you to build little experiments and mini-apps with
Electron. Each Fiddle has at least three of these files: A main script, a
renderer script, a preload script, and an HTML file.
</p>
<p>
If you <code>require()</code> a module, Fiddle will install it
automatically. It will also automatically provide you with
autocomplete information for the <code>electron</code> module.
If you <code>require()</code> a module, Fiddle will install
it automatically. It will also automatically provide you with autocomplete
information for the <code>electron</code> module.
</p>
</>
),
)
},
{
name: 'select-versions',
@@ -47,21 +47,25 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Electron Fiddle knows about all released Electron versions,
downloading your versions automatically in the background.
Electron Fiddle knows about all released Electron versions, downloading
your versions automatically in the background.
</p>
<p>
Open the preferences to see all available versions and delete those
previously downloaded.
Open the preferences to see all available versions and delete those previously
downloaded.
</p>
</>
),
)
},
{
name: 'button-run',
selector: '.button-run',
title: '🚀 Run Your Fiddle',
content: <p>Hit this button to give your Fiddle a try and start it.</p>,
content: (
<p>
Hit this button to give your Fiddle a try and start it.
</p>
)
},
{
name: 'button-publish',
@@ -70,17 +74,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Like what you&apos;ve built? You can save your Fiddle as a public
GitHub Gist, allowing other users to load it by pasting the URL into
the address bar. If they don&apos;t have Electron Fiddle, they can
see and download your code directly from GitHub.
Like what you've built? You can save your Fiddle as a public GitHub Gist,
allowing other users to load it by pasting the URL into the address bar.
If they don't have Electron Fiddle, they can see and download your code
directly from GitHub.
</p>
<p>
You can also package your Fiddle as a standalone binary or as an
installer from the &quot;Tasks&quot; menu.
You can also package your Fiddle as a standalone binary or as an installer
from the "Tasks" menu.
</p>
</>
),
)
},
{
name: 'first-time-electron',
@@ -88,25 +92,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
title: '👋 Getting Started With Electron?',
content: (
<p>
We&apos;ve finished our tour of Electron Fiddle, but if this is your
We've finished our tour of Electron Fiddle, but if this is your
first time using Electron, we could introduce you to its basics.
Interested?
</p>
),
getButtons: ({
stop,
advance,
}: TourStepGetButtonParams): Array<JSX.Element> => {
getButtons: ({ stop, advance }: TourStepGetButtonParams): Array<JSX.Element> => {
return [
<Button key="btn-stop" onClick={stop} text="I'm good!" icon="stop" />,
<Button
key="btn-adv"
onClick={advance}
text="Electron Basics"
icon="help"
/>,
<Button key='btn-stop' onClick={stop} text="I'm good!" icon='stop' />,
<Button key='btn-adv' onClick={advance} text='Electron Basics' icon='help' />
];
},
}
},
{
name: 'main-editor',
@@ -115,24 +111,24 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Every Electron app starts with a main script, very similar to how a
Node.js application is started. The main script runs in the
&quot;main process&quot;. To display a user interface, the main
process creates renderer processes  usually in the form of windows,
which Electron calls &nbsp;<code>BrowserWindow</code>.
Every Electron app starts with a main script, very similar to how
a Node.js application is started. The main script runs in the "main
process". To display a user interface, the main process creates renderer
processes  usually in the form of windows, which Electron calls
&nbsp;<code>BrowserWindow</code>.
</p>
<p>
To get started, pretend that the main process is just like a Node.js
process. All APIs and features found in Electron are accessible
through the <code>electron</code> module, which can be required like
any other Node.js module.
process. All APIs and features found in Electron are accessible through
the <code>electron</code> module, which can be required like any other
Node.js module.
</p>
<p>
The default fiddle creates a new <code>BrowserWindow</code> and
loads an HTML file.
The default fiddle creates a new <code>BrowserWindow</code> and loads
an HTML file.
</p>
</>
),
)
},
{
name: 'html-editor',
@@ -140,14 +136,14 @@ export function getWelcomeTour(): Set<TourScriptStep> {
title: '📝 HTML',
content: (
<p>
In the default fiddle, this HTML file is loaded in the &nbsp;
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works in
a browser will work here, too. In addition, Electron allows you to
execute Node.js code. Take a close look at the &nbsp;
<code>&lt;script /&gt;</code> tag and notice how we can call{' '}
<code>require()</code> like we would in Node.js.
In the default fiddle, this HTML file is loaded in the
&nbsp;<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
in a browser will work here, too. In addition, Electron allows you
to execute Node.js code. Take a close look at the
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
),
)
},
{
name: 'renderer-editor',
@@ -156,19 +152,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
This is the script we just required from the HTML file. In here, you
can do anything that works in Node.js <i>and</i> anything that works
in a browser.
This is the script we just required from the HTML file. In here, you can
do anything that works in Node.js <i>and</i> anything that works in a browser.
</p>
<p>
By the way: If you want to use an <code>npm</code> module here, just
&nbsp;<code>require</code> it. Electron Fiddle will automatically
detect that you requested a module and install it as soon as you run
your fiddle.
&nbsp;<code>require</code> it. Electron Fiddle will automatically detect that you
requested a module and install it as soon as you run your fiddle.
</p>
</>
),
},
)
}
]);
}
@@ -180,10 +174,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
* @extends {React.Component<WelcomeTourProps, WelcomeTourState>}
*/
@observer
export class WelcomeTour extends React.Component<
WelcomeTourProps,
WelcomeTourState
> {
export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourState> {
constructor(props: WelcomeTourProps) {
super(props);
@@ -191,7 +182,7 @@ export class WelcomeTour extends React.Component<
this.startTour = this.startTour.bind(this);
this.state = {
isTourStarted: false,
isTourStarted: false
};
}
@@ -213,16 +204,16 @@ export class WelcomeTour extends React.Component<
return (
<>
<Button
key="cancel"
key='cancel'
onClick={this.stopTour}
icon="cross"
icon='cross'
text={`I'll figure it out`}
/>
<Button
key="ok"
key='ok'
onClick={this.startTour}
icon="presentation"
text="Show me around"
icon='presentation'
text='Show me around'
/>
</>
);
@@ -236,27 +227,34 @@ export class WelcomeTour extends React.Component<
if (!isTourStarted) {
return (
<Dialog key="welcome-tour-dialog" isOpen={true}>
<Dialog
key='welcome-tour-dialog'
isOpen={true}
>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>🙋 Hey There!</h4>
</div>
<div className={Classes.DIALOG_BODY}>
<p>
Welcome to Electron Fiddle! If you&apos;re new to the app,
we&apos;d like to give you a brief tour of its features.
Welcome to Electron Fiddle! If you're new to the app,
we'd like to give you a brief tour of its features.
</p>
<p>
We won&apos;t show this dialog again, but you can always find the
tour in the Help menu.
We won't show this dialog again, but you can always
find the tour in the Help menu.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>{this.buttons}</div>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{this.buttons}
</div>
</div>
</Dialog>
);
} else {
return <Tour tour={getWelcomeTour()} onStop={this.stopTour} />;
return (
<Tour tour={getWelcomeTour()} onStop={this.stopTour} />
);
}
}
}
+48 -57
View File
@@ -40,7 +40,7 @@ export class Tour extends React.Component<TourProps, TourState> {
this.state = {
tour: props.tour.entries(),
step: null,
i: 0,
i: 0
};
}
@@ -92,7 +92,11 @@ export class Tour extends React.Component<TourProps, TourState> {
if (!step) return null;
return <div className="tour">{this.getStep(step)}</div>;
return (
<div className='tour'>
{this.getStep(step)}
</div>
);
}
/**
@@ -108,34 +112,36 @@ export class Tour extends React.Component<TourProps, TourState> {
if (getButtons) {
return getButtons({
stop: this.stop,
advance: this.advance,
advance: this.advance
});
}
// No? Fine! Are we at the end of the tour?
return this.props.tour.size === this.state.i
? [
? [(
<Button
icon='tick-circle'
onClick={this.stop}
key='btn-stop'
text='Finish Tour'
/>
)] : [
(
<Button
icon="tick-circle"
icon='stop'
key='btn-stop'
onClick={this.stop}
key="btn-stop"
text="Finish Tour"
/>,
]
: [
text='Stop Tour'
/>
), (
<Button
icon="stop"
key="btn-stop"
onClick={this.stop}
text="Stop Tour"
/>,
<Button
icon="step-forward"
key="btn-adv"
icon='step-forward'
key='btn-adv'
onClick={this.advance}
text="Continue"
/>,
];
text='Continue'
/>
)
];
}
/**
@@ -145,10 +151,7 @@ export class Tour extends React.Component<TourProps, TourState> {
* @param {ClientRect} rect
* @returns {JSX.Element}
*/
private getDialogForStep(
step: TourScriptStep,
rect: ClientRect,
): JSX.Element {
private getDialogForStep(step: TourScriptStep, rect: ClientRect): JSX.Element {
const buttons = this.getButtons(step);
const size = { width: 400, height: 300 };
const margin = 10;
@@ -167,15 +170,21 @@ export class Tour extends React.Component<TourProps, TourState> {
key={step.name}
isOpen={true}
style={style}
portalClassName="tour-portal"
backdropProps={{ style: { visibility: 'hidden' } }}
portalClassName='tour-portal'
backdropProps={{ style: { visibility: 'hidden' }}}
>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>{step.title}</h4>
<h4 className={Classes.HEADING}>
{step.title}
</h4>
</div>
<div className={Classes.DIALOG_BODY}>
{step.content}
</div>
<div className={Classes.DIALOG_BODY}>{step.content}</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>{buttons}</div>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{buttons}
</div>
</div>
</Dialog>
);
@@ -196,38 +205,20 @@ export class Tour extends React.Component<TourProps, TourState> {
return (
<>
{this.getDialogForStep(step, rect)}
<svg height="100%" width="100%">
<rect
fill="rgba(0, 0, 0, 0.65)"
x="0"
y="0"
mask="url(#mask)"
height="100%"
width="100%"
/>
<mask
id="mask"
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse"
>
<rect
className="bg"
x="0"
y="0"
fill="white"
height="100%"
width="100%"
/>
<svg height='100%' width='100%'>
<rect fill='rgba(0, 0, 0, 0.65)' x='0' y='0' mask='url(#mask)' height='100%' width='100%'/>
<mask id='mask' maskUnits='userSpaceOnUse' maskContentUnits='userSpaceOnUse'>
<rect className='bg' x='0' y='0' fill='white' height='100%' width='100%' />
<rect
x={left}
y={top}
width={width}
height={height}
fill="black"
rx="5"
ry="5"
strokeWidth="5"
stroke="black"
fill='black'
rx='5'
ry='5'
strokeWidth='5'
stroke='black'
/>
</mask>
</svg>
+7 -20
View File
@@ -44,9 +44,7 @@ export function getItemLabel({ source, state, name }: RunnableVersion): string {
export function getItemIcon({ state }: RunnableVersion) {
return state === 'ready'
? 'saved'
: state === 'downloading'
? 'cloud-download'
: 'cloud';
: state === 'downloading' ? 'cloud-download' : 'cloud';
}
/**
@@ -57,10 +55,7 @@ export function getItemIcon({ state }: RunnableVersion) {
* @param {RunnableVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<RunnableVersion> = (
query,
{ version },
) => {
export const filterItem: ItemPredicate<RunnableVersion> = (query, { version }) => {
return version.toLowerCase().includes(query.toLowerCase());
};
@@ -72,10 +67,7 @@ export const filterItem: ItemPredicate<RunnableVersion> = (
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<RunnableVersion> = (
item,
{ handleClick, modifiers, query },
) => {
export const renderItem: ItemRenderer<RunnableVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
@@ -103,9 +95,7 @@ export interface VersionSelectProps {
currentVersion: RunnableVersion;
onVersionSelect: (version: RunnableVersion) => void;
buttonGroupProps?: IButtonGroupProps;
itemDisabled?:
| keyof RunnableVersion
| ((item: RunnableVersion, index: number) => boolean);
itemDisabled?: keyof RunnableVersion | ((item: RunnableVersion, index: number) => boolean);
}
/**
@@ -116,10 +106,7 @@ export interface VersionSelectProps {
* @extends {React.Component<VersionSelectProps, VersionSelectState>}
*/
@observer
export class VersionSelect extends React.Component<
VersionSelectProps,
VersionSelectState
> {
export class VersionSelect extends React.Component<VersionSelectProps, VersionSelectState> {
public render() {
const { currentVersion, itemDisabled } = this.props;
const { version } = currentVersion;
@@ -132,11 +119,11 @@ export class VersionSelect extends React.Component<
itemPredicate={filterItem}
itemDisabled={itemDisabled}
onItemSelect={this.props.onVersionSelect}
noResults={<MenuItem disabled={true} text="No results." />}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!this.props.disabled}
>
<Button
className="version-chooser"
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentVersion)}
disabled={!!this.props.disabled}
+4 -10
View File
@@ -8,10 +8,7 @@ import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
// Reminder: When testing, this file is mocked in tests/setup.js
export const USER_DATA_PATH = remote.app.getPath('userData');
export const CONFIG_PATH = path.join(
remote.app.getPath('home'),
'.electron-fiddle',
);
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'row',
@@ -20,13 +17,10 @@ export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'column',
first: EditorId.renderer,
second: EditorId.html,
},
}
};
export const DEFAULT_CLOSED_PANELS: Partial<Record<
MosaicId,
EditorBackup | true
>> = {
export const DEFAULT_CLOSED_PANELS: Partial<Record<MosaicId, EditorBackup | true>> = {
docsDemo: true,
preload: getEditorBackup(EditorId.preload),
css: getEditorBackup(EditorId.css),
@@ -35,7 +29,7 @@ export const DEFAULT_CLOSED_PANELS: Partial<Record<
export const EMPTY_EDITOR_CONTENT = {
html: '<!-- Empty -->',
js: '// Empty',
css: '/* Empty */',
css: '/* Empty */'
};
export const ELECTRON_ORG = 'electron';
+4 -7
View File
@@ -42,18 +42,15 @@ export async function getContent(
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
const values = await window.ElectronFiddle.app.getEditorValues({
include: false,
});
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
// Handle main case, which needs to check both possible versions
if (name === EditorId.main) {
const isChanged1x =
(await getContent(EditorId.main, '1.0')) === values.main;
const isChangedOther = (await getContent(EditorId.main)) === values.main;
const isChanged1x = await getContent(EditorId.main, '1.0') === values.main;
const isChangedOther = await getContent(EditorId.main) === values.main;
return isChanged1x || isChangedOther;
} else {
return values[name] === (await getContent(name));
return values[name] === await getContent(name);
}
}
+13 -28
View File
@@ -56,6 +56,7 @@ export async function removeTypeDefsForVersion(version: string) {
}
}
/**
* Get the path for offline TypeScript definitions
*
@@ -72,9 +73,7 @@ export function getOfflineTypeDefinitionPath(version: string): string {
* @param {string} version
* @returns {boolean}
*/
export async function getOfflineTypeDefinitions(
version: string,
): Promise<boolean> {
export async function getOfflineTypeDefinitions(version: string): Promise<boolean> {
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(getOfflineTypeDefinitionPath(version));
}
@@ -86,12 +85,10 @@ export async function getOfflineTypeDefinitions(
* @param {string} version
* @returns {void}
*/
export async function getDownloadedVersionTypeDefs(
version: RunnableVersion,
): Promise<string | null> {
export async function getDownloadedVersionTypeDefs(version: RunnableVersion): Promise<string | null> {
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
if (await getOfflineTypeDefinitions(version.version)) {
try {
@@ -131,10 +128,7 @@ export async function getLocalVersionTypeDefs(version: RunnableVersion) {
*
* @param {string} version
*/
export async function updateEditorTypeDefinitions(
version: RunnableVersion,
i = 0,
): Promise<void> {
export async function updateEditorTypeDefinitions(version: RunnableVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -142,23 +136,18 @@ export async function updateEditorTypeDefinitions(
}
console.warn(`Fetch Types: Called too soon, deferring`);
return callIn(i * 100 + 200, () =>
updateEditorTypeDefinitions(version, i + 1),
);
return callIn(i * 100 + 200, () => updateEditorTypeDefinitions(version, i + 1));
};
// If this method is called before we're ready, we'll delay this work a bit
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco)
return defer();
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco) return defer();
const { app } = window.ElectronFiddle;
const monaco: typeof MonacoType = app.monaco!;
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
const getTypeDefs =
version.source === VersionSource.local
? getLocalVersionTypeDefs
: getDownloadedVersionTypeDefs;
const getTypeDefs = (version.source === VersionSource.local) ?
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
const typeDefs = await getTypeDefs(version);
@@ -167,12 +156,8 @@ export async function updateEditorTypeDefinitions(
}
if (typeDefs) {
console.log(
`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`,
);
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(
typeDefs,
);
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`);
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs);
window.ElectronFiddle.app.typeDefDisposable = disposable;
} else {
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
@@ -187,7 +172,7 @@ export function getLocalTypePathForVersion(version: RunnableVersion) {
'electron',
'tsc',
'typings',
'electron.d.ts',
'electron.d.ts'
);
} else {
return null;
+14 -34
View File
@@ -3,14 +3,7 @@ import * as path from 'path';
import { EditorValues, Files, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PACKAGE_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
import { fancyImport } from '../utils/import';
import { ipcRendererManager } from './ipc';
@@ -41,12 +34,9 @@ export class FileManager {
this.saveFiddle(filePath, dotfilesTransform);
});
ipcRendererManager.on(
IpcEvents.FS_SAVE_FIDDLE_FORGE,
(_event, filePath) => {
this.saveFiddle(filePath, dotfilesTransform, forgeTransform);
},
);
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_FORGE, (_event, filePath) => {
this.saveFiddle(filePath, dotfilesTransform, forgeTransform);
});
}
/**
@@ -57,9 +47,7 @@ export class FileManager {
*/
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {
templateName,
});
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
@@ -80,10 +68,11 @@ export class FileManager {
main: await this.readFile(path.join(filePath, MAIN_JS_NAME)),
renderer: await this.readFile(path.join(filePath, RENDERER_JS_NAME)),
preload: await this.readFile(path.join(filePath, PRELOAD_JS_NAME)),
css: await this.readFile(path.join(filePath, STYLES_CSS_NAME)),
css: await this.readFile(path.join(filePath, STYLES_CSS_NAME))
};
window.ElectronFiddle.app.replaceFiddle(editorValues, { filePath });
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
}
/**
@@ -93,10 +82,7 @@ export class FileManager {
* @param {string} filePath
* @memberof FileManager
*/
public async saveFiddle(
filePath?: string,
...transforms: Array<FileTransform>
) {
public async saveFiddle(filePath?: string, ...transforms: Array<FileTransform>) {
const { localPath } = this.appState;
const pathToSave = filePath || localPath;
@@ -121,13 +107,13 @@ export class FileManager {
if (pathToSave !== localPath) {
this.appState.localPath = pathToSave;
this.appState.gistId = undefined;
}
this.appState.isUnsaved = false;
}
}
/**
* Get files to save, but with a transform applied
*
@@ -136,10 +122,7 @@ export class FileManager {
* @returns {Promise<Files>}
* @memberof FileManager
*/
public async getFiles(
options?: PackageJsonOptions,
...transforms: Array<FileTransform>
): Promise<Files> {
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
let output: Files = new Map();
@@ -156,16 +139,14 @@ export class FileManager {
console.log(`getFiles: Applying ${transform.name}`);
output = await transform(output);
} catch (error) {
console.warn(`getFiles: Failed to apply transform`, {
transform,
error,
});
console.warn(`getFiles: Failed to apply transform`, { transform, error });
}
}
return output;
}
/**
* Attempts to clean a given directory. Used to manually
* clean temp directories.
@@ -198,8 +179,7 @@ export class FileManager {
* @returns {Promise<string>}
*/
public async saveToTemp(
options: PackageJsonOptions,
...transforms: Array<FileTransform>
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const tmp = await import('tmp');
+37 -60
View File
@@ -1,70 +1,53 @@
import { EditorValues } from '../interfaces';
import { exec } from '../utils/exec';
const { builtinModules } = require('module');
const { builtinModules } = require('module');
export type IPackageManager = 'npm' | 'yarn';
export interface PMOperationOptions {
export interface NpmOperationOptions {
dir: string;
packageManager: IPackageManager;
}
export let isNpmInstalled: boolean | null = null;
export let isYarnInstalled: boolean | null = null;
export let isInstalled: boolean | null = null;
/* add other modules to automatically ignore here */
/* perhaps we can expose this to the settings module?*/
const ignoredModules: Array<string> = [
'electron',
'original-fs',
...builtinModules,
...builtinModules
];
/* regular expression to both match and extract module names */
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
Quick and dirty filter functions for filtering module names
*/
const isIgnored = (str: string): boolean => ignoredModules.includes(str);
const isLocalModule = (str: string): boolean => /^[,/~\.]/.test(str);
const isLocalModule = (str: string): boolean => (/^[,/~\.]/.test(str));
const isUnique = (item: any, idx: number, arr: Array<any>): boolean => {
return arr.lastIndexOf(item) === idx;
};
/**
* Checks if package manager is installed by checking if a binary
* Checks if npm is installed by checking if a binary
* with that name can be found.
*/
export async function getIsPackageManagerInstalled(
packageManager: IPackageManager,
ignoreCache?: boolean,
): Promise<boolean> {
if (packageManager === 'npm' && isNpmInstalled !== null && !ignoreCache)
return isNpmInstalled;
if (packageManager === 'yarn' && isYarnInstalled !== null && !ignoreCache)
return isYarnInstalled;
export async function getIsNpmInstalled(ignoreCache?: boolean): Promise<boolean> {
if (isInstalled !== null && !ignoreCache) return isInstalled;
const command =
process.platform === 'win32'
? `where.exe ${packageManager}`
: `which ${packageManager}`;
const command = process.platform === 'win32'
? 'where.exe npm'
: 'which npm';
try {
await exec(process.cwd(), command);
if (packageManager === 'npm') {
return (isNpmInstalled = true);
} else {
return (isYarnInstalled = true);
}
return isInstalled = true;
} catch (error) {
console.warn(`getIsPackageManagerInstalled: "${command}" failed.`, error);
if (packageManager === 'npm') {
return (isNpmInstalled = false);
} else {
return (isYarnInstalled = false);
}
console.warn(`getIsNpmInstalled: "${command}" failed.`, error);
return isInstalled = false;
}
}
@@ -75,10 +58,13 @@ export async function getIsPackageManagerInstalled(
* @returns {Array<string>}
*/
export function findModulesInEditors(values: EditorValues) {
const files = [values.main, values.renderer];
const files = [ values.main, values.renderer ];
const modules = files.reduce(
(agg, file) => [...agg, ...findModules(file)],
[],
(agg, file) => [
...agg,
...findModules(file)
],
[]
);
console.log('Modules Found:', modules);
@@ -105,7 +91,8 @@ export function findModules(input: string): Array<string> {
let match: RegExpMatchArray | null;
/* grab all global require matches in the text */
while ((match = requiregx.exec(input) || null)) {
// tslint:disable-next-line:no-conditional-assignment
while (match = (requiregx.exec(input) || null)) {
// ensure commented-out requires aren't downloaded
if (!match[0].startsWith('//')) {
const mod = match[1];
@@ -116,7 +103,9 @@ export function findModules(input: string): Array<string> {
/* map and reduce */
return modules
.map((mod) =>
mod.includes('/') && !mod.startsWith('@') ? mod.split('/')[0] : mod,
mod.includes('/') && !mod.startsWith('@') ?
mod.split('/')[0] :
mod
)
.filter((m) => !isIgnored(m))
.filter((m) => !isLocalModule(m))
@@ -126,37 +115,25 @@ export function findModules(input: string): Array<string> {
/**
* Installs given modules to a given folder.
*
* @param {PMOperationOptions} { dir, packageManager }
* @param {NpmOperationOptions} { dir }
* @param {...Array<string>} names
* @returns {Promise<string>}
*/
export async function installModules(
{ dir, packageManager }: PMOperationOptions,
...names: Array<string>
): Promise<string> {
let nameArgs: Array<string> = [];
export async function installModules({ dir }: NpmOperationOptions, ...names: Array<string>): Promise<string> {
const nameArgs = names.length > 0
? [ '-S', ...names ]
: ['--dev --prod'];
if (packageManager === 'npm') {
nameArgs = names.length > 0 ? ['-S', ...names] : ['--dev --prod'];
} else {
nameArgs = [...names];
}
const installCommand = packageManager === 'npm' ? 'npm install' : 'yarn add';
return exec(dir, [installCommand].concat(nameArgs).join(' '));
return exec(dir, [ `npm install` ].concat(nameArgs).join(' '));
}
/**
* Execute an "{packageManager} run" command
* Execute an "npm run" command
*
* @param {PMOperationOptions} { dir, packageManager }
* @param {NpmOperationOptions} { dir }
* @param {string} command
* @returns {Promise<string>}
*/
export function packageRun(
{ dir, packageManager }: PMOperationOptions,
command: string,
): Promise<string> {
return exec(dir, `${packageManager} run ${command}`);
export function npmRun({ dir }: NpmOperationOptions, command: string): Promise<string> {
return exec(dir, `npm run ${command}`);
}
+37 -95
View File
@@ -1,13 +1,7 @@
import { Octokit } from '@octokit/rest';
import { when } from 'mobx';
import { EditorId, EditorValues, GenericDialogType } from '../interfaces';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
@@ -29,16 +23,11 @@ export class RemoteLoader {
this.handleLoadingFailed.bind(this);
}
public async loadFiddleFromElectronExample(
_: any,
exampleInfo: { path: string; ref: string },
) {
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const { path, ref } = exampleInfo;
const prettyName = path.replace('docs/fiddles/', '');
const ok = await this.verifyRemoteLoad(
`'${prettyName}' example from the Electron docs for version ${ref}`,
);
const ok = await this.verifyRemoteLoad(`'${prettyName}' example from the Electron docs for version ${ref}`);
if (!ok) return;
this.fetchExampleAndLoad(ref, path);
@@ -52,10 +41,7 @@ export class RemoteLoader {
this.fetchGistAndLoad(id);
}
public async fetchExampleAndLoad(
ref: string,
path: string,
): Promise<boolean> {
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
@@ -74,14 +60,12 @@ export class RemoteLoader {
renderer: await getContent(EditorId.renderer, this.appState.version),
main: await getContent(EditorId.main, this.appState.version),
preload: await getContent(EditorId.preload, this.appState.version),
css: await getContent(EditorId.css, this.appState.version),
css: await getContent(EditorId.css, this.appState.version)
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error(
'The example Fiddle tried to launch is not a valid Electron example',
);
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
@@ -92,52 +76,32 @@ export class RemoteLoader {
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.main = t;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.main = t; })
);
break;
case INDEX_HTML_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.html = t;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.html = t; })
);
break;
case RENDERER_JS_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.renderer = t;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.renderer = t; })
);
break;
case PRELOAD_JS_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.preload = t;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.preload = t; })
);
case STYLES_CSS_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.css = t;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.css = t; })
);
break;
@@ -162,10 +126,7 @@ export class RemoteLoader {
* @returns {string}
* @memberof RemoteLoader
*/
public getContentOrEmpty(
gist: Octokit.Response<Octokit.GistsGetResponse>,
name: string,
): string {
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
try {
return gist.data.files[name].content;
} catch (error) {
@@ -184,16 +145,13 @@ export class RemoteLoader {
const octo = await getOctokit(this.appState);
const gist = await octo.gists.get({ gist_id: gistId });
return this.handleLoadingSuccess(
{
html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME),
css: this.getContentOrEmpty(gist, STYLES_CSS_NAME),
},
gistId,
);
return this.handleLoadingSuccess({
html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME),
css: this.getContentOrEmpty(gist, STYLES_CSS_NAME)
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
@@ -202,21 +160,14 @@ export class RemoteLoader {
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
const version = await this.getPackageVersionFromRef(ref);
const supportedVersions = sortedElectronMap(
this.appState.versions,
(k) => k,
);
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
if (!supportedVersions.includes(version)) {
this.handleLoadingFailed(
new Error('Version of Electron in example not supported'),
);
this.handleLoadingFailed(new Error('Version of Electron in example not supported'));
return false;
}
// check if version is part of release channel
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(
version,
);
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
@@ -235,23 +186,17 @@ export class RemoteLoader {
owner: ELECTRON_ORG,
repo: ELECTRON_REPO,
ref,
path: 'package.json',
path: 'package.json'
});
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
const packageJsonString = Buffer.from(
packageJsonData.content,
'base64',
).toString('utf8');
const packageJsonString = Buffer.from(packageJsonData.content, 'base64').toString('utf8');
const { version } = JSON.parse(packageJsonString);
return version;
} else {
console.error(
`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`,
{
packageJsonData,
},
);
console.error(`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`, {
packageJsonData
});
return '0.0.0';
}
@@ -265,7 +210,7 @@ export class RemoteLoader {
public async verifyRemoteLoad(what: string): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.confirm,
label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`,
label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
@@ -278,7 +223,7 @@ export class RemoteLoader {
type: GenericDialogType.warning,
label: `You're loading an example with a version of Electron with an unincluded release
channel (${channel}). Do you want to enable the release channel to load the
version of Electron from the example?`,
version of Electron from the example?`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
@@ -293,10 +238,7 @@ export class RemoteLoader {
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(
values: Partial<EditorValues>,
gistId: string,
): Promise<boolean> {
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, { gistId });
return true;
}
@@ -313,13 +255,13 @@ export class RemoteLoader {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed: ${error}`,
cancel: undefined,
cancel: undefined
});
} else {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined,
cancel: undefined
});
}
+34 -68
View File
@@ -7,18 +7,12 @@ import { PackageJsonOptions } from '../utils/get-package';
import { maybePlural } from '../utils/plural-maybe';
import { getElectronBinaryPath, getIsDownloaded } from './binary';
import { ipcRendererManager } from './ipc';
import {
findModulesInEditors,
getIsPackageManagerInstalled,
installModules,
packageRun,
PMOperationOptions,
} from './npm';
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
import { AppState } from './state';
export enum ForgeCommands {
PACKAGE = 'package',
MAKE = 'make',
MAKE = 'make'
}
export class Runner {
@@ -32,6 +26,7 @@ export class Runner {
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_PACKAGE);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_MAKE);
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
this.performForgeOperation(ForgeCommands.PACKAGE);
@@ -59,12 +54,11 @@ export class Runner {
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
const packageManager = this.appState.packageManager;
if (!dir) return false;
try {
await this.installModulesForEditor(values, { dir, packageManager });
await this.installModulesForEditor(values, dir);
} catch (error) {
console.error('Runner: Could not install modules', error);
fileManager.cleanup(dir);
@@ -108,49 +102,39 @@ export class Runner {
* @returns {Promise<boolean>}
* @memberof Runner
*/
public async performForgeOperation(
operation: ForgeCommands,
): Promise<boolean> {
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
const options = { includeDependencies: true, includeElectron: true };
const { dotfilesTransform } = await import('./transforms/dotfiles');
const { forgeTransform } = await import('./transforms/forge');
const { pushError, pushOutput } = this.appState;
const strings =
operation === ForgeCommands.MAKE
? ['Creating installers for', 'Binary']
: ['Packaging', 'Installers'];
const strings = operation === ForgeCommands.MAKE
? [ 'Creating installers for', 'Binary' ]
: [ 'Packaging', 'Installers' ];
this.appState.isConsoleShowing = true;
pushOutput(`📦 ${strings[0]} current Fiddle...`);
const packageManager = this.appState.packageManager;
const pmInstalled = await getIsPackageManagerInstalled(packageManager);
if (!pmInstalled) {
let message = `Error: Could not find ${packageManager}. Fiddle requires Node.js and npm or yarn `;
if (!(await getIsNpmInstalled())) {
let message = `Error: Could not find npm. Fiddle requires Node.js and npm `;
message += `to compile packages. Please visit https://nodejs.org to install `;
message += `Node.js and npm, or https://classic.yarnpkg.com/lang/en/ `;
message += `to install Yarn`;
message += `Node.js and npm.`;
this.appState.pushOutput(message, { isNotPre: true });
return false;
}
// Save files to temp
const dir = await this.saveToTemp(
options,
dotfilesTransform,
forgeTransform,
);
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
if (!dir) return false;
// Files are now saved to temp, let's install Forge and dependencies
if (!(await this.packageInstall({ dir, packageManager }))) return false;
if (!(await this.npmInstall(dir))) return false;
// Cool, let's run "package"
try {
console.log(`Now creating ${strings[1].toLowerCase()}...`);
pushOutput(await packageRun({ dir, packageManager }, operation));
pushOutput(await npmRun({ dir }, operation));
pushOutput(`${strings[1]} successfully created.`, { isNotPre: true });
} catch (error) {
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
@@ -170,36 +154,24 @@ export class Runner {
* @param {string} dir
* @returns {Promise<void>}
*/
public async installModulesForEditor(
values: EditorValues,
pmOptions: PMOperationOptions,
): Promise<void> {
const modules = findModulesInEditors(values);
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
const modules = await findModulesInEditors(values);
const { pushOutput } = this.appState;
if (modules && modules.length > 0) {
const packageManager = pmOptions.packageManager;
const pmInstalled = await getIsPackageManagerInstalled(packageManager);
if (!pmInstalled) {
let message = `The ${maybePlural(`module`, modules)} ${modules.join(
', ',
)} need to be installed, `;
message += `but we could not find ${packageManager}. Fiddle requires Node.js and npm `;
if (!(await getIsNpmInstalled())) {
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
message += `but we could not find npm. Fiddle requires Node.js and npm `;
message += `to support the installation of modules not included in `;
message += `Electron. Please visit https://nodejs.org to install Node.js `;
message += `and npm, or https://classic.yarnpkg.com/lang/en/ to install Yarn`;
message += `and npm.`;
pushOutput(message, { isNotPre: true });
return;
}
pushOutput(
`Installing node modules using ${
pmOptions.packageManager
}: ${modules.join(', ')}...`,
{ isNotPre: true },
);
pushOutput(await installModules(pmOptions, ...modules));
pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
pushOutput(await installModules({ dir }, ...modules));
}
}
@@ -229,21 +201,18 @@ export class Runner {
}
// Add user-specified cli flags if any have been set.
const options = [dir, '--inspect'].concat(this.appState.executionFlags);
const options = [ dir, '--inspect' ].concat(this.appState.executionFlags);
this.child = spawn(binaryPath, options, { cwd: dir, env });
this.appState.isRunning = true;
pushOutput(`Electron v${version} started.`);
this.child.stdout!.on('data', (data) =>
pushOutput(data, { bypassBuffer: false }),
);
this.child.stderr!.on('data', (data) =>
pushOutput(data, { bypassBuffer: false }),
);
this.child.stdout!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.on('close', async (code) => {
const withCode =
typeof code === 'number' ? ` with code ${code.toString()}.` : `.`;
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
: `.`;
pushOutput(`Electron exited${withCode}`);
this.appState.isRunning = false;
@@ -264,8 +233,7 @@ export class Runner {
* @memberof Runner
*/
public async saveToTemp(
options: PackageJsonOptions,
...transforms: Array<FileTransform>
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string | null> {
const { fileManager } = window.ElectronFiddle.app;
const { pushOutput, pushError } = this.appState;
@@ -284,16 +252,16 @@ export class Runner {
/**
* Installs modules in a given directory (we're basically
* just running "{packageManager} install")
* just running "npm install")
*
* @param {PMOperationOptions} options
* @param {string} dir
* @returns
* @memberof Runner
*/
public async packageInstall(options: PMOperationOptions): Promise<boolean> {
public async npmInstall(dir: string): Promise<boolean> {
try {
this.appState.pushOutput(`Now running "npm install..."`);
this.appState.pushOutput(await installModules(options));
this.appState.pushOutput(await installModules({ dir }));
return true;
} catch (error) {
this.appState.pushError('Failed to run "npm install".', error);
@@ -307,9 +275,7 @@ export class Runner {
*/
private async deleteUserData() {
if (this.appState.isKeepingUserDataDirs) {
console.log(
`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`,
);
console.log(`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`);
return;
}
+95 -191
View File
@@ -8,45 +8,32 @@ import {
EditorId,
GenericDialogOptions,
GenericDialogType,
GistActionState,
MosaicId,
OutputEntry,
OutputOptions,
RunnableVersion,
Version,
VersionSource,
VersionState,
VersionState
} from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { arrayToStringMap } from '../utils/array-to-stringmap';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
import {
createMosaicArrangement,
getVisibleMosaics,
} from '../utils/editors-mosaic-arrangement';
import { createMosaicArrangement, getVisibleMosaics } from '../utils/editors-mosaic-arrangement';
import { getName } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
import {
getDownloadedVersions,
getDownloadingVersions,
removeBinary,
setupBinary,
} from './binary';
import { getDownloadedVersions, getDownloadingVersions, removeBinary, setupBinary } from './binary';
import { Bisector } from './bisect';
import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import {
getLocalTypePathForVersion,
updateEditorTypeDefinitions,
} from './fetch-types';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import { waitForEditorsToMount } from '../utils/editor-mounted';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { IPackageManager } from './npm';
import {
addLocalVersion,
ElectronReleaseChannel,
@@ -54,7 +41,7 @@ import {
getElectronVersions,
getReleaseChannel,
getUpdatedElectronVersions,
saveLocalVersions,
saveLocalVersions
} from './versions';
const knownVersions = getElectronVersions();
@@ -65,16 +52,14 @@ const defaultVersion = getDefaultVersion(knownVersions);
* easier, we keep them around in a global object. Don't judge us,
* we're really only doing that for the editors.
*/
window.ElectronFiddle =
window.ElectronFiddle ||
({
editors: {
main: null,
renderer: null,
html: null,
},
app: null,
} as any);
window.ElectronFiddle = window.ElectronFiddle || {
editors: {
main: null,
renderer: null,
html: null
},
app: null
} as any;
/**
* The application's state. Exported as a singleton below.
@@ -86,98 +71,58 @@ export class AppState {
// -- Persisted settings ------------------
@observable public version: string = defaultVersion;
@observable public theme: string | null = localStorage.getItem('theme');
@observable public gitHubAvatarUrl: string | null = localStorage.getItem(
'gitHubAvatarUrl',
);
@observable public gitHubName: string | null = localStorage.getItem(
'gitHubName',
);
@observable public gitHubLogin: string | null = localStorage.getItem(
'gitHubLogin',
);
@observable public gitHubToken: string | null =
localStorage.getItem('gitHubToken') || null;
@observable public gitHubPublishAsPublic = !!this.retrieve(
'gitHubPublishAsPublic',
);
@observable public channelsToShow: Array<
ElectronReleaseChannel
> = (this.retrieve('channelsToShow') as Array<ElectronReleaseChannel>) || [
ElectronReleaseChannel.stable,
ElectronReleaseChannel.beta,
];
@observable public statesToShow: Array<VersionState> = (this.retrieve(
'statesToShow',
) as Array<VersionState>) || [
VersionState.downloading,
VersionState.ready,
VersionState.unknown,
];
@observable public isKeepingUserDataDirs = !!this.retrieve(
'isKeepingUserDataDirs',
);
@observable public isEnablingElectronLogging = !!this.retrieve(
'isEnablingElectronLogging',
);
@observable public isClearingConsoleOnRun = !!this.retrieve(
'isClearingConsoleOnRun',
);
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
@observable public gitHubToken: string | null = localStorage.getItem('gitHubToken') || null;
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
@observable public channelsToShow: Array<ElectronReleaseChannel> =
this.retrieve('channelsToShow') as Array<ElectronReleaseChannel>
|| [ElectronReleaseChannel.stable, ElectronReleaseChannel.beta];
@observable public statesToShow: Array<VersionState> =
this.retrieve('statesToShow') as Array<VersionState>
|| [VersionState.downloading, VersionState.ready, VersionState.unknown];
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
@observable public executionFlags: Array<string> =
(this.retrieve('executionFlags') as Array<string>) === null
? []
: (this.retrieve('executionFlags') as Array<string>);
@observable public packageManager: IPackageManager =
(localStorage.getItem('packageManager') as IPackageManager) || 'npm';
this.retrieve('executionFlags') as Array<string> === null ?
[] : this.retrieve('executionFlags') as Array<string>;
// -- Various session-only state ------------------
@observable public gistId: string | undefined;
@observable public versions: Record<
string,
RunnableVersion
> = arrayToStringMap(knownVersions);
@observable public gistId: string = '';
@observable public versions: Record<string, RunnableVersion> = arrayToStringMap(knownVersions);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | undefined;
@observable public genericDialogOptions: GenericDialogOptions = {
type: GenericDialogType.warning,
label: '' as string | JSX.Element,
ok: 'Okay',
cancel: 'Cancel',
wantsInput: false,
placeholder: '',
};
@observable public genericDialogOptions = { type: GenericDialogType.warning, label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public genericDialogLastResult: boolean | null = null;
@observable public genericDialogLastInput: string | null = null;
@observable public mosaicArrangement: MosaicNode<
MosaicId
> | null = DEFAULT_MOSAIC_ARRANGEMENT;
@observable public mosaicArrangement: MosaicNode<MosaicId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
@observable public templateName: string | undefined;
@observable public currentDocsDemoPage: DocsDemoPage = DocsDemoPage.DEFAULT;
@observable public localTypeWatcher: fsType.FSWatcher | undefined;
@observable public Bisector: Bisector | undefined;
@observable public activeGistAction: GistActionState = GistActionState.none;
@observable public isRunning = false;
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean;
@observable public isUpdatingElectronVersions = false;
@observable public isQuitting = false;
@observable public isUpdatingElectronVersions: boolean = false;
@observable public isQuitting: boolean = false;
// -- Various "isShowing" settings ------------------
@observable public isBisectCommandShowing: boolean;
@observable public isConsoleShowing = false;
@observable public isTokenDialogShowing = false;
@observable public isGenericDialogShowing = false;
@observable public isSettingsShowing = false;
@observable public isBisectDialogShowing = false;
@observable public isAddVersionDialogShowing = false;
@observable public isThemeDialogShowing = false;
@observable public isTourShowing = !localStorage.getItem('hasShownTour');
@observable public isConsoleShowing: boolean = false;
@observable public isTokenDialogShowing: boolean = false;
@observable public isGenericDialogShowing: boolean = false;
@observable public isSettingsShowing: boolean = false;
@observable public isBisectDialogShowing: boolean = false;
@observable public isAddVersionDialogShowing: boolean = false;
@observable public isThemeDialogShowing: boolean = false;
@observable public isTourShowing: boolean = !localStorage.getItem('hasShownTour');
// -- Editor Values stored when we close the editor ------------------
@observable public closedPanels: Partial<
Record<MosaicId, EditorBackup | true>
> = DEFAULT_CLOSED_PANELS;
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = DEFAULT_CLOSED_PANELS;
private outputBuffer = '';
private outputBuffer: string = '';
private name: string;
public appData: string;
@@ -208,38 +153,24 @@ export class AppState {
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
ipcRendererManager.on(IpcEvents.CLEAR_CONSOLE, this.clearConsole);
ipcRendererManager.on(
IpcEvents.BISECT_COMMANDS_TOGGLE,
this.toggleBisectCommands,
);
ipcRendererManager.on(IpcEvents.BISECT_COMMANDS_TOGGLE, this.toggleBisectCommands);
ipcRendererManager.on(IpcEvents.BEFORE_QUIT, this.setIsQuitting);
ipcRendererManager.once(IpcEvents.SET_APPDATA_DIR, (_event, dir) => {
this.appData = dir;
});
ipcRendererManager.once(IpcEvents.SET_APPDATA_DIR, (_event, dir) => { this.appData = dir; });
// Setup auto-runs
autorun(() => this.save('theme', this.theme));
autorun(() =>
this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun),
);
autorun(() => this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun));
autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl));
autorun(() => this.save('gitHubLogin', this.gitHubLogin));
autorun(() => this.save('gitHubName', this.gitHubName));
autorun(() => this.save('gitHubToken', this.gitHubToken));
autorun(() =>
this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic),
);
autorun(() =>
this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs),
);
autorun(() =>
this.save('isEnablingElectronLogging', this.isEnablingElectronLogging),
);
autorun(() => this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic));
autorun(() => this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs));
autorun(() => this.save('isEnablingElectronLogging', this.isEnablingElectronLogging));
autorun(() => this.save('executionFlags', this.executionFlags));
autorun(() => this.save('version', this.version));
autorun(() => this.save('channelsToShow', this.channelsToShow));
autorun(() => this.save('statesToShow', this.statesToShow));
autorun(() => this.save('packageManager', this.packageManager ?? 'npm'));
autorun(() => {
if (typeof this.isUnsaved === 'undefined') return;
@@ -250,7 +181,7 @@ export class AppState {
this.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
ok: 'Quit',
ok: 'Quit'
});
this.isGenericDialogShowing = true;
@@ -272,6 +203,7 @@ export class AppState {
}
});
// return value doesn't matter, we just want to cancel the event
return false;
};
@@ -313,26 +245,24 @@ export class AppState {
* current settings for states and channels to display
*/
@computed get versionsToShow(): Array<RunnableVersion> {
return sortedElectronMap<RunnableVersion>(
this.versions,
(_key, item) => item,
).filter((item) => {
if (!item) {
return false;
}
return sortedElectronMap<RunnableVersion>(this.versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!this.channelsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the version
if (!this.channelsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!this.statesToShow.includes(item.state)) {
return false;
}
// Check if we want to show the state
if (!this.statesToShow.includes(item.state)) {
return false;
}
return true;
});
return true;
});
}
/**
@@ -405,7 +335,9 @@ export class AppState {
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
(document.activeElement as HTMLInputElement).blur();
if ((document.activeElement as HTMLInputElement).blur) {
(document.activeElement as HTMLInputElement).blur();
}
this.resetView({ isSettingsShowing: !this.isSettingsShowing });
}
@@ -431,11 +363,10 @@ export class AppState {
@action public setGenericDialogOptions(opts: GenericDialogOptions) {
this.genericDialogOptions = {
type: GenericDialogType.warning,
ok: 'Okay',
cancel: 'Cancel',
wantsInput: false,
placeholder: '',
...opts,
...opts
};
}
@@ -471,9 +402,9 @@ export class AppState {
if (release && release.source === VersionSource.local) {
delete updatedVersions[version];
const versionsAsArray = Object.keys(updatedVersions).map(
(k) => updatedVersions[k],
);
const versionsAsArray = Object
.keys(updatedVersions)
.map((k) => updatedVersions[k]);
saveLocalVersions(versionsAsArray);
} else {
@@ -509,9 +440,7 @@ export class AppState {
await setupBinary(this, version);
this.updateDownloadedVersionState();
} else {
console.log(
`State: Version ${version} already downloaded, doing nothing.`,
);
console.log(`State: Version ${version} already downloaded, doing nothing.`);
}
}
@@ -525,9 +454,7 @@ export class AppState {
const version = normalizeVersion(input);
if (!this.versions[version]) {
console.warn(
`State: Called setVersion() with ${version}, which does not exist.`,
);
console.warn(`State: Called setVersion() with ${version}, which does not exist.`);
this.setVersion(knownVersions[0].version);
return;
@@ -549,14 +476,10 @@ export class AppState {
if (versionObject.source === VersionSource.local) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typePath = getLocalTypePathForVersion(versionObject);
console.info(
`TypeDefs: Watching file for local version ${version} at path ${typePath}`,
);
console.info(`TypeDefs: Watching file for local version ${version} at path ${typePath}`);
try {
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(
`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`,
);
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
await updateEditorTypeDefinitions(versionObject);
});
} catch (err) {
@@ -564,9 +487,7 @@ export class AppState {
}
} else {
if (!!this.localTypeWatcher) {
console.info(
`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`,
);
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
this.localTypeWatcher.close();
this.localTypeWatcher = undefined;
}
@@ -624,8 +545,7 @@ export class AppState {
* @param {(string | Buffer)} data
*/
@action public pushOutput(
data: string | Buffer,
options: OutputOptions = { isNotPre: false, bypassBuffer: true },
data: string | Buffer, options: OutputOptions = { isNotPre: false, bypassBuffer: true }
) {
let strData = data.toString();
const { isNotPre, bypassBuffer } = options;
@@ -654,7 +574,7 @@ export class AppState {
this.output.push({
timestamp: Date.now(),
text: strData.trim(),
isNotPre,
isNotPre
});
}
@@ -676,9 +596,7 @@ export class AppState {
*
* @param {EditorId} id
*/
@action public getAndRemoveEditorValueBackup(
id: EditorId,
): EditorBackup | null {
@action public getAndRemoveEditorValueBackup(id: EditorId): EditorBackup | null {
const value = this.closedPanels[id];
if (isEditorBackup(value)) {
@@ -694,7 +612,9 @@ export class AppState {
for (const id of ALL_MOSAICS) {
if (!visible.includes(id) && currentlyVisible.includes(id)) {
this.closedPanels[id] = isEditorId(id) ? getEditorBackup(id) : true;
this.closedPanels[id] = isEditorId(id)
? getEditorBackup(id)
: true;
// if we have backup, remove active editor
delete window.ElectronFiddle.editors[id];
@@ -702,21 +622,13 @@ export class AppState {
// Remove the backup for panels now. Editors will remove their
// backup once the data has been loaded.
if (
isPanelId(id) &&
visible.includes(id) &&
!currentlyVisible.includes(id)
) {
if (isPanelId(id) && visible.includes(id) && !currentlyVisible.includes(id)) {
delete this.closedPanels[id];
}
}
const updatedArrangement = createMosaicArrangement(visible);
console.log(
`State: Setting visible mosaic panels`,
visible,
updatedArrangement,
);
console.log(`State: Setting visible mosaic panels`, visible, updatedArrangement);
this.mosaicArrangement = updatedArrangement;
@@ -804,19 +716,11 @@ export class AppState {
* @param {string} key
* @param {(string | number | object)} [value]
*/
private save(
key: string,
value?:
| string
| number
| Array<any>
| Record<string, unknown>
| null
| boolean,
) {
private save(key: string, value?: string | number | object | null | boolean) {
if (value) {
const _value =
typeof value === 'object' ? JSON.stringify(value) : value.toString();
const _value = typeof value === 'object'
? JSON.stringify(value)
: value.toString();
localStorage.setItem(key, _value);
} else {
+3 -12
View File
@@ -2,13 +2,7 @@ import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { EditorValues } from '../interfaces';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { fancyImport } from '../utils/import';
/**
@@ -34,10 +28,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
if (fs.existsSync(templatesPath)) {
const contents = fs.readdirSync(templatesPath);
console.log(
`getTemplateValues(): ${templatesPath} contents:`,
contents,
);
console.log(`getTemplateValues(): ${templatesPath} contents:`, contents);
} else {
console.log(`getTemplateValues(): ${templatesPath} does not exist`);
}
@@ -51,6 +42,6 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
main: await getFile(MAIN_JS_NAME),
html: await getFile(INDEX_HTML_NAME),
preload: await getFile(PRELOAD_JS_NAME),
css: await getFile(STYLES_CSS_NAME),
css: await getFile(STYLES_CSS_NAME)
};
}
+10 -8
View File
@@ -53,16 +53,17 @@ export const defaultDark: LoadedFiddleTheme = {
'text-color-2': '#1e2527',
'text-color-3': '#dcdcdc',
'error-color': '#df3434',
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
// tslint:disable-next-line:max-line-length
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`
},
editor: {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#2f3241',
},
},
'editor.background': '#2f3241'
}
}
};
export const defaultLight: LoadedFiddleTheme = {
@@ -85,17 +86,18 @@ export const defaultLight: LoadedFiddleTheme = {
'text-color-2': '#1e2527',
'text-color-3': '#0e0e0e',
'error-color': '#df3434',
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
// tslint:disable-next-line:max-line-length
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`
},
editor: {
base: 'vs',
inherit: true,
rules: [],
colors: {},
},
colors: {}
}
};
export enum DefaultThemes {
DARK = 'defaultDark',
LIGHT = 'defaultLight',
LIGHT = 'defaultLight'
}
+11 -23
View File
@@ -4,13 +4,7 @@ import * as path from 'path';
import { fancyImport } from '../utils/import';
import { CONFIG_PATH } from './constants';
import {
defaultDark,
defaultLight,
DefaultThemes,
FiddleTheme,
LoadedFiddleTheme,
} from './themes-defaults';
import { defaultDark, defaultLight, DefaultThemes, FiddleTheme, LoadedFiddleTheme } from './themes-defaults';
export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
@@ -21,12 +15,10 @@ export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
* @param {LoadedFiddleTheme} [theme]
*/
export async function activateTheme(
monaco?: typeof MonacoType,
theme?: LoadedFiddleTheme,
name?: string | null,
monaco?: typeof MonacoType, theme?: LoadedFiddleTheme, name?: string | null
) {
const _monaco = monaco || window.ElectronFiddle.app.monaco;
const _theme = theme || (await getTheme(name));
const _theme = theme || await getTheme(name);
if (!_monaco || !_monaco.editor) return;
@@ -34,6 +26,7 @@ export async function activateTheme(
_monaco.editor.setTheme('main');
}
/**
* Read in a theme file.
*
@@ -41,9 +34,7 @@ export async function activateTheme(
* @param {string} [name]
* @returns {Promise<FiddleTheme>}
*/
export async function readThemeFile(
name?: string,
): Promise<LoadedFiddleTheme | null> {
export async function readThemeFile(name?: string): Promise<LoadedFiddleTheme | null> {
if (!name || name === DefaultThemes.DARK) return defaultDark as any;
if (name === DefaultThemes.LIGHT) return defaultLight as any;
@@ -56,7 +47,7 @@ export async function readThemeFile(
return {
...theme,
name: theme.name || name.replace('.json', ''),
file,
file
};
} catch (error) {
console.warn(`Themes: Loading theme ${name} failed`, error);
@@ -64,6 +55,7 @@ export async function readThemeFile(
}
}
/**
* Reads and then returns all available themes.
*
@@ -73,7 +65,7 @@ export async function getAvailableThemes(): Promise<Array<LoadedFiddleTheme>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const themes: Array<LoadedFiddleTheme> = [
defaultDark as any,
defaultLight as any,
defaultLight as any
];
if (!fs.existsSync(THEMES_PATH)) {
@@ -105,11 +97,9 @@ export async function getAvailableThemes(): Promise<Array<LoadedFiddleTheme>> {
* @param {string} [name]
* @returns {Promise<FiddleTheme>}
*/
export async function getTheme(
name?: string | null,
): Promise<LoadedFiddleTheme> {
export async function getTheme(name?: string | null): Promise<LoadedFiddleTheme> {
console.log(`Themes: getTheme() loading ${name || 'default'}`);
const theme = (await readThemeFile(name || undefined)) || defaultDark;
const theme = await readThemeFile(name || undefined) || defaultDark;
return { ...theme, css: await getCssStringForTheme(theme) };
}
@@ -120,9 +110,7 @@ export async function getTheme(
* @param {FiddleTheme} theme
* @returns {string}
*/
export async function getCssStringForTheme(
theme: FiddleTheme,
): Promise<string> {
export async function getCssStringForTheme(theme: FiddleTheme): Promise<string> {
let cssContent = '';
Object.keys(theme.common).forEach((key) => {
+46 -35
View File
@@ -1,7 +1,7 @@
import {
remote,
TouchBarButtonConstructorOptions,
TouchBarScrubberConstructorOptions,
TouchBarScrubberConstructorOptions
} from 'electron';
import { autorun } from 'mobx';
@@ -15,7 +15,7 @@ const {
TouchBarButton,
TouchBarScrubber,
TouchBarSpacer,
TouchBarLabel,
TouchBarLabel
} = TouchBar;
/**
@@ -26,9 +26,11 @@ const {
* @returns {string}
*/
export function getItemIcon(
{ state }: Partial<RunnableVersion> = { state: VersionState.unknown },
{ state }: Partial<RunnableVersion> = { state: VersionState.unknown }
) {
return state === 'ready' ? '💾' : state === 'downloading' ? '⏬' : '☁';
return state === 'ready'
? '💾'
: state === 'downloading' ? '⏬' : '☁';
}
export class TouchBarManager {
@@ -38,28 +40,31 @@ export class TouchBarManager {
public touchBar: Electron.TouchBar;
public versionSelector: Electron.TouchBarScrubber;
public versionSelectorBtn: Electron.TouchBarButton;
public selectedVersion = '';
public selectedVersion: string = '';
public versionSelectorSelectBtn: Electron.TouchBarButton;
// Lol have mercy, TS
public items: Array<Electron.TouchBarButton
| Electron.TouchBarColorPicker
| Electron.TouchBarGroup
| Electron.TouchBarLabel
| Electron.TouchBarPopover
| Electron.TouchBarScrubber
| Electron.TouchBarSegmentedControl
| Electron.TouchBarSlider
| Electron.TouchBarSpacer>
| undefined;
constructor(public readonly appState: AppState) {
this.selectVersion = this.selectVersion.bind(this);
this.updateStartStopBtn = this.updateStartStopBtn.bind(this);
this.updateVersionSelectorItems = this.updateVersionSelectorItems.bind(
this,
);
this.updateVersionSelectorItems = this.updateVersionSelectorItems.bind(this);
this.updateVersionButton = this.updateVersionButton.bind(this);
this.startStopBtn = new TouchBarButton(this.getStartStopButtonOptions());
this.consoleBtn = new TouchBarButton(this.getConsoleButtonOptions());
this.versionSelectorBtn = new TouchBarButton(
this.getVersionButtonOptions(),
);
this.versionSelectorSelectBtn = new TouchBarButton(
this.getVersionSelectorSelectButtonOptions(),
);
this.versionSelector = new TouchBarScrubber(
this.getVersionSelectorOptions(),
);
this.versionSelectorBtn = new TouchBarButton(this.getVersionButtonOptions());
this.versionSelectorSelectBtn = new TouchBarButton(this.getVersionSelectorSelectButtonOptions());
this.versionSelector = new TouchBarScrubber(this.getVersionSelectorOptions());
this.setupTouchBar();
}
@@ -120,9 +125,8 @@ export class TouchBarManager {
*/
public setTouchBar(options: Partial<Electron.TouchBarConstructorOptions>) {
try {
this.touchBar = new TouchBar(
options as Electron.TouchBarConstructorOptions,
);
this.items = options.items;
this.touchBar = new TouchBar(options as Electron.TouchBarConstructorOptions);
this.browserWindow = this.browserWindow || remote.getCurrentWindow();
this.browserWindow.setTouchBar(this.touchBar);
} catch (error) {
@@ -138,8 +142,8 @@ export class TouchBarManager {
items: [
new TouchBarSpacer({ size: 'flexible' }),
new TouchBarLabel({ label: getNiceGreeting() }),
new TouchBarSpacer({ size: 'flexible' }),
],
new TouchBarSpacer({ size: 'flexible' })
]
});
}
@@ -153,8 +157,8 @@ export class TouchBarManager {
this.consoleBtn,
new TouchBarSpacer({ size: 'flexible' }),
this.versionSelectorBtn,
new TouchBarSpacer({ size: 'flexible' }),
],
new TouchBarSpacer({ size: 'flexible' })
]
});
}
@@ -163,8 +167,13 @@ export class TouchBarManager {
*/
public setVersionSelectorItems() {
this.setTouchBar({
items: [this.versionSelector, this.versionSelectorSelectBtn],
escapeItem: new TouchBarButton(this.getVersionSelectorEscButtonOptions()),
items: [
this.versionSelector,
this.versionSelectorSelectBtn,
],
escapeItem: new TouchBarButton(
this.getVersionSelectorEscButtonOptions()
)
});
}
@@ -177,7 +186,7 @@ export class TouchBarManager {
label: `Back`,
click: () => {
this.setDefaultItems();
},
}
};
}
@@ -194,7 +203,7 @@ export class TouchBarManager {
}
this.setDefaultItems();
},
}
};
}
@@ -205,12 +214,13 @@ export class TouchBarManager {
* @returns {TouchBarButtonConstructorOptions}
*/
public getStartStopButtonOptions(): TouchBarButtonConstructorOptions {
const label = this.appState.isRunning ? '🛑 Stop' : '🚀 Run';
const label = this.appState.isRunning
? '🛑 Stop'
: '🚀 Run';
const click = () =>
this.appState.isRunning
? window.ElectronFiddle.app.runner.stop()
: window.ElectronFiddle.app.runner.run();
const click = () => this.appState.isRunning
? window.ElectronFiddle.app.runner.stop()
: window.ElectronFiddle.app.runner.run();
return { label, click };
}
@@ -245,7 +255,8 @@ export class TouchBarManager {
*
* @returns {TouchBarScrubberConstructorOptions}
*/
public getVersionSelectorOptions(): TouchBarScrubberConstructorOptions {
public getVersionSelectorOptions(
): TouchBarScrubberConstructorOptions {
return {
select: this.selectVersion,
highlight: this.selectVersion,
@@ -254,7 +265,7 @@ export class TouchBarManager {
overlayStyle: 'outline',
mode: 'free',
continuous: true,
showArrowButtons: false,
showArrowButtons: false
};
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Files } from '../../interfaces';
* @returns {Promise<Files>}
*/
export async function dotfilesTransform(files: Files): Promise<Files> {
files.set('.gitignore', 'node_modules\nout');
files.set('.gitignore', 'node_modules\nout');
return files;
}
+11 -10
View File
@@ -15,12 +15,11 @@ export async function forgeTransform(files: Files): Promise<Files> {
// devDependencies
parsed.devDependencies = parsed.devDependencies || {};
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-squirrel'] =
'6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-squirrel'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.34';
// Scripts
parsed.scripts = parsed.scripts || {};
@@ -42,16 +41,18 @@ export async function forgeTransform(files: Files): Promise<Files> {
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
platforms: [
'darwin'
]
},
{
name: '@electron-forge/maker-deb',
config: {},
config: {}
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
config: {}
}
];
files.set(PACKAGE_NAME, JSON.stringify(parsed, undefined, 2));
+19 -41
View File
@@ -1,17 +1,12 @@
import semver from 'semver';
import {
RunnableVersion,
Version,
VersionSource,
VersionState,
} from '../interfaces';
import { RunnableVersion, Version, VersionSource, VersionState } from '../interfaces';
import { normalizeVersion } from '../utils/normalize-version';
export const enum ElectronReleaseChannel {
stable = 'Stable',
beta = 'Beta',
nightly = 'Nightly',
unsupported = 'Unsupported',
unsupported = 'Unsupported'
}
/**
@@ -21,25 +16,18 @@ export const enum ElectronReleaseChannel {
* @returns {string}
*/
export function getDefaultVersion(
knownVersions: Array<RunnableVersion> = [],
knownVersions: Array<RunnableVersion> = []
): string {
const ls = localStorage.getItem('version');
if (
ls &&
knownVersions &&
knownVersions.find(({ version }) => version === ls)
) {
if (ls && knownVersions && knownVersions.find(({ version }) => version === ls)) {
return ls;
}
// Self-heal: Version not formated correctly
const normalized = ls && normalizeVersion(ls);
if (normalized) {
if (
knownVersions &&
knownVersions.find(({ version }) => version === normalized)
) {
if (knownVersions && knownVersions.find(({ version }) => version === normalized)) {
return normalized;
}
}
@@ -62,9 +50,9 @@ export function getDefaultVersion(
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: Version | string,
input: Version | string
): ElectronReleaseChannel {
const tag = typeof input === 'string' ? input : input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
@@ -84,7 +72,7 @@ export function getReleaseChannel(
export const enum VersionKeys {
local = 'local-electron-versions',
known = 'known-electron-versions',
known = 'known-electron-versions'
}
/**
@@ -95,8 +83,7 @@ export const enum VersionKeys {
* @returns {Array<Version>}
*/
function getVersions(
key: VersionKeys,
fallbackMethod: () => Array<Version>,
key: VersionKeys, fallbackMethod: () => Array<Version>
): Array<Version> {
const fromLs = window.localStorage.getItem(key);
@@ -107,9 +94,7 @@ function getVersions(
if (!isExpectedFormat(result)) {
// Known versions can just be downloaded again.
if (key === VersionKeys.known) {
throw new Error(
`Electron versions in LS does not match expected format`,
);
throw new Error(`Electron versions in LS does not match expected format`);
}
// Local versions are a bit more tricky and might be in an old format (pre 0.5)
@@ -119,9 +104,7 @@ function getVersions(
return result;
} catch (error) {
console.warn(
`Parsing local Electron versions failed, returning fallback method.`,
);
console.warn(`Parsing local Electron versions failed, returning fallback method.`);
}
}
@@ -149,7 +132,7 @@ export function getElectronVersions(): Array<RunnableVersion> {
return {
...version,
source: VersionSource.remote,
state: VersionState.unknown,
state: VersionState.unknown
};
});
@@ -157,7 +140,7 @@ export function getElectronVersions(): Array<RunnableVersion> {
return {
...version,
source: VersionSource.local,
state: VersionState.ready,
state: VersionState.ready
};
});
@@ -217,9 +200,7 @@ export function saveLocalVersions(versions: Array<Version | RunnableVersion>) {
* @returns {Array<Version>}
*/
export function getKnownVersions(): Array<Version> {
return getVersions(VersionKeys.known, () =>
require('../../static/releases.json'),
);
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
}
/**
@@ -238,9 +219,8 @@ export function saveKnownVersions(versions: Array<Version>) {
* @export
* @returns {Promise<Array<RunnableVersion>>}
*/
export async function getUpdatedElectronVersions(): Promise<
Array<RunnableVersion>
> {
export async function getUpdatedElectronVersions(
): Promise<Array<RunnableVersion>> {
try {
await fetchVersions();
} catch (error) {
@@ -256,9 +236,7 @@ export async function getUpdatedElectronVersions(): Promise<
* @returns {Promise<Array<Version>>}
*/
export async function fetchVersions() {
const response = await window.fetch(
'https://unpkg.com/electron-releases/lite.json',
);
const response = await window.fetch('https://unpkg.com/electron-releases/lite.json');
const data = await response.json();
// pre-0.24.0 versions were technically 'atom-shell' and cannot
@@ -304,14 +282,14 @@ export function migrateVersions(input: Array<any> = []): Array<Version> {
return {
version: tag_name,
name,
localPath: url,
localPath: url
};
})
.filter((item) => !!item) as Array<Version>;
}
export function isElectronVersion(
input: Version | RunnableVersion,
input: Version | RunnableVersion
): input is RunnableVersion {
return (input as RunnableVersion).source !== undefined;
}
+2 -5
View File
@@ -1,10 +1,7 @@
import * as Sentry from '@sentry/electron';
import { isDevMode } from './utils/devmode';
export function initSentry() {
if (!(global as any).__JEST__ && !isDevMode()) {
Sentry.init({
dsn: 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540',
});
if (!(global as any).__JEST__) {
Sentry.init({dsn: 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540'});
}
}
+2 -2
View File
@@ -29,6 +29,6 @@ export const SHOW_ME_TEMPLATES: Templates = {
TouchBar: 'TouchBar',
Tray: 'Tray',
WebContents: 'WebContents',
WebFrame: 'WebFrame',
},
WebFrame: 'WebFrame'
}
};
+1 -1
View File
@@ -9,7 +9,7 @@ import { normalizeVersion } from './normalize-version';
* @returns {Record<string, RunnableVersion>}
*/
export function arrayToStringMap(
input: Array<RunnableVersion>,
input: Array<RunnableVersion>
): Record<string, RunnableVersion> {
const output = {};
+2 -4
View File
@@ -1,3 +1,4 @@
/**
* Call this method in a certain number of milliseconds. Returns
* a promise that resolves with the passed function.
@@ -7,10 +8,7 @@
* @param {(...args: Array<any>) => T} fn
* @returns {Promise<T>}
*/
export function callIn<T>(
ms: number,
fn: (...args: Array<any>) => T,
): Promise<T> {
export function callIn<T>(ms: number, fn: (...args: Array<any>) => T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => resolve(fn()), ms);
});
+1 -1
View File
@@ -8,6 +8,6 @@ export function getDocsUrlForModule(moduleName: string) {
return {
full: `https://electronjs.org/docs/api/${moduleName}`,
repo: `https://github.com/electron/electron/blob/master/docs/api/${moduleName}.md`,
short: `electronjs.org/docs/api/${moduleName}`,
short: `electronjs.org/docs/api/${moduleName}`
};
}
+1 -1
View File
@@ -21,6 +21,6 @@ export function getEditorBackup(id: EditorId): EditorBackup {
return {
value: getEditorValue(id),
model: getEditorModel(id),
viewState: getEditorViewState(id),
viewState: getEditorViewState(id)
};
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { EditorId } from '../interfaces';
* @param {EditorId} id
* @returns {editor.ITextModel | null}
*/
export function getEditorModel(id: EditorId): editor.ITextModel | null {
export function getEditorModel(id: EditorId): editor.ITextModel | null {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
+2 -6
View File
@@ -10,17 +10,13 @@ export function waitForEditorsToMount(editors: Array<MosaicId>) {
const interval = 100;
return new Promise((resolve, reject) => {
(function checkMountedEditors() {
const allMounted = editors.every(
(id) => !!window.ElectronFiddle.editors[id],
);
const allMounted = editors.every((id) => !!window.ElectronFiddle.editors[id]);
if (allMounted) {
return resolve();
}
time += interval;
if (time > maxTime) {
return reject(
`Timed out after ${maxTime}ms: can't mount editors onto mosaics.`,
);
return reject(`Timed out after ${maxTime}ms: can't mount editors onto mosaics.`);
}
setTimeout(checkMountedEditors, 100);
})();
+1 -3
View File
@@ -9,9 +9,7 @@ import { EditorId } from '../interfaces';
* @param {EditorId} id
* @returns {(editor.ICodeEditorViewState | null)}
*/
export function getEditorViewState(
id: EditorId,
): editor.ICodeEditorViewState | null {
export function getEditorViewState(id: EditorId): editor.ICodeEditorViewState | null {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
+6 -9
View File
@@ -10,21 +10,20 @@ import { MosaicId } from '../interfaces';
* @returns {MosaicNode<MosaicId>}
*/
export function createMosaicArrangement(
input: Array<MosaicId>,
direction: MosaicDirection = 'row',
input: Array<MosaicId>, direction: MosaicDirection = 'row'
): MosaicNode<MosaicId> {
if (input.length === 1) {
return input[0];
}
// This cuts out the first half of input. Input becomes the second half.
const secondHalf = [...input];
const secondHalf = [ ...input ];
const firstHalf = secondHalf.splice(0, Math.floor(secondHalf.length / 2));
return {
direction,
first: createMosaicArrangement(firstHalf, 'column'),
second: createMosaicArrangement(secondHalf, 'column'),
second: createMosaicArrangement(secondHalf, 'column')
};
}
@@ -35,20 +34,18 @@ export function createMosaicArrangement(
* @param {MosaicNode<MosaicId> | null} input
* @returns {Array<MosaicId>}
*/
export function getVisibleMosaics(
input: MosaicNode<MosaicId> | null,
): Array<MosaicId> {
export function getVisibleMosaics(input: MosaicNode<MosaicId> | null): Array<MosaicId> {
// Handle the unlikely null case
if (!input) return [];
// Handle the case where only one editor is visible
if (typeof input === 'string') {
return [input];
return [ input ];
}
// Handle the other cases (2 - 4)
const result = [];
for (const node of [input.first, input.second]) {
for (const node of [ input.first, input.second ]) {
if (typeof node === 'string') {
result.push(node);
} else {
+9 -13
View File
@@ -14,20 +14,16 @@ export async function exec(dir: string, cliArgs: string): Promise<string> {
const { exec: cpExec } = await import('child_process');
cpExec(
cliArgs,
{
cwd: dir,
maxBuffer: 200 * 1024 * 100, // 100 times the default
},
(error, result) => {
if (error) {
reject(error);
}
cpExec(cliArgs, {
cwd: dir,
maxBuffer: 200 * 1024 * 100 // 100 times the default
}, (error, result) => {
if (error) {
reject(error);
}
resolve(typeof result === 'string' ? result : `${result}`);
},
);
resolve(typeof result === 'string' ? result : `${result}`);
});
});
}
+7 -4
View File
@@ -5,12 +5,15 @@ import * as MonacoType from 'monaco-editor';
*
* @returns {(MonacoType.editor.IStandaloneCodeEditor | null)}
*/
export function getFocusedEditor(): MonacoType.editor.IStandaloneCodeEditor | null {
export function getFocusedEditor(
): MonacoType.editor.IStandaloneCodeEditor | null {
const focusedKey = Object.keys(window.ElectronFiddle.editors).find((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor =
window.ElectronFiddle.editors[key];
const editor: MonacoType.editor.IStandaloneCodeEditor
= window.ElectronFiddle.editors[key];
return editor && editor.hasTextFocus && editor.hasTextFocus();
});
return focusedKey ? window.ElectronFiddle.editors[focusedKey] : null;
return focusedKey
? window.ElectronFiddle.editors[focusedKey]
: null;
}
+15 -21
View File
@@ -11,7 +11,7 @@ export interface PackageJsonOptions {
export const DEFAULT_OPTIONS = {
includeElectron: true,
includeDependencies: true,
includeDependencies: true
};
/**
@@ -23,9 +23,7 @@ export const DEFAULT_OPTIONS = {
* @returns {string}
*/
export async function getPackageJson(
appState: AppState,
values?: EditorValues,
options?: PackageJsonOptions,
appState: AppState, values?: EditorValues, options?: PackageJsonOptions
): Promise<string> {
const { includeElectron, includeDependencies } = options || DEFAULT_OPTIONS;
const name = await appState.getName();
@@ -44,22 +42,18 @@ export async function getPackageJson(
});
}
return JSON.stringify(
{
name,
productName: name,
description: 'My Electron application description',
keywords: [],
main: './main.js',
version: '1.0.0',
author: getUsername(),
scripts: {
start: 'electron .',
},
dependencies,
devDependencies,
return JSON.stringify({
name,
productName: name,
description: 'My Electron application description',
keywords: [],
main: './main.js',
version: '1.0.0',
author: getUsername(),
scripts: {
start: 'electron .'
},
undefined,
2,
);
dependencies,
devDependencies
}, undefined, 2);
}
+2 -2
View File
@@ -1,6 +1,6 @@
import * as os from 'os';
let username = '';
let username: string = '';
/**
* Returns the curren username
@@ -9,5 +9,5 @@ let username = '';
* @returns {string}
*/
export function getUsername(): string {
return (username = username || os.userInfo().username);
return username = username || os.userInfo().username;
}
+2 -2
View File
@@ -21,6 +21,6 @@ export function idFromUrl(input: string): string | null {
* @param {string} input
* @returns {string}
*/
export function urlFromId(input?: string): string {
return input ? `https://gist.github.com/${input}` : '';
export function urlFromId(input: string): string {
return `https://gist.github.com/${input}`;
}
+2 -5
View File
@@ -12,10 +12,7 @@ import * as React from 'react';
* @param {string} query
* @returns
*/
export function highlightText(
text: string,
query: string,
): Array<React.ReactNode | string> | null {
export function highlightText(text: string, query: string): Array<React.ReactNode | string> | null {
let lastIndex = 0;
const words = query
@@ -23,7 +20,7 @@ export function highlightText(
.filter((word) => word.length > 0)
.map((s) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
if (words.length === 0) return [text];
if (words.length === 0) return [ text ];
const regexp = new RegExp(words.join('|'), 'gi');
const tokens: Array<React.ReactNode> = [];
+3 -1
View File
@@ -6,7 +6,9 @@
* @returns {any}
*/
export function getAtPath(input: string, obj: any): any {
return input.split('.').reduce((o, s) => o[s], obj);
return input
.split('.')
.reduce((o, s) => o[s], obj);
}
/**
+1 -1
View File
@@ -4,7 +4,7 @@
* @param {string} version
* @returns {string}
*/
export function normalizeVersion(version = ''): string {
export function normalizeVersion(version: string = ''): string {
if (version.startsWith('v')) {
return version.slice(1);
} else {

Some files were not shown because too many files have changed in this diff Show More