Compare commits

...
352 Commits
Author SHA1 Message Date
Felix Rieseberg ebf421ebed wip: Properly select Node version 2019-12-12 15:20:13 -05:00
Felix Rieseberg 41409f578e wip: Even more stuff 2019-12-12 14:58:34 -05:00
Felix Rieseberg f32fce2f80 Merge pull request #281 from electron/fix-dropdowns
fix: Incorrect dropdowns
2019-12-12 14:56:25 -05:00
Felix Rieseberg 26e054702a fix: Incorrect dropdowns 2019-12-12 14:23:11 -05:00
Felix Rieseberg 16f27132cb Revert "First basis for having Node in there, too"
This reverts commit 7613176f69.
2019-12-12 14:06:34 -05:00
Felix RiesebergandTierney Cyren 7613176f69 First basis for having Node in there, too
Co-authored-by: Tierney Cyren <ticyren@microsoft.com>
2019-12-11 12:19:16 -05:00
dependabot[bot] 470bea4fca build(deps): bump serialize-to-js from 3.0.0 to 3.0.1 (#280)
Bumps [serialize-to-js](https://github.com/commenthol/serialize-to-js) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/commenthol/serialize-to-js/releases)
- [Commits](https://github.com/commenthol/serialize-to-js/compare/v3.0.0...v3.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-10 00:30:59 -08:00
Felix Rieseberg 363febdb73 Merge pull request #279 from electron/notarize
build: Notarize the build with Apple
2019-12-04 13:43:38 -08:00
Felix Rieseberg 062ca4e205 build: Update to electron-forge b46 2019-12-04 10:44:13 -08:00
Felix Rieseberg 49053342a5 Merge pull request #278 from electron/electron-713
chore: Update Electron to 7.1.3
2019-12-04 10:09:10 -08:00
Felix Rieseberg fd582c67a8 build: Use a later macOS image 2019-12-03 18:08:51 -08:00
Felix Rieseberg 706b18808b build: Notarize the build with Apple 2019-12-03 17:52:52 -08:00
Felix Rieseberg a6ae8d5376 chore: Update Electron to 7.1.3 2019-12-03 17:45:02 -08:00
Erick Zhao 5034f8c853 feat: add bisect helper (#257) 2019-11-22 18:46:58 -05:00
Letra e916247013 fix: ensure dev tools only open in dev mode (#277) 2019-11-15 09:22:14 -05:00
Felix Rieseberg c052686de4 0.11.1 2019-11-12 14:39:38 +01:00
Erick Zhao 31aacef131 fix: update folder selection dialog for Electron 7 (#271) 2019-11-05 16:51:04 -05:00
Erick Zhao 932369b705 fix: update deprecated app.setName API (#273) 2019-11-05 16:24:43 -05:00
Shelley Vohr b835c34786 fix: update dialog example for promisification (#272) 2019-11-03 11:36:00 -05:00
Felix Rieseberg c9e1ba815a 0.11.0 2019-10-31 11:49:23 -04:00
Felix Rieseberg 374a85e2f6 Merge pull request #267 from electron/updates-updates
feat: Electron 7 (and more updates)
2019-10-27 11:26:14 -07:00
Felix Rieseberg 4282d0b93d fix: Better typing 2019-10-27 11:16:55 -07:00
Felix Rieseberg 549862b3bb chore: npm audit fix 2019-10-27 11:13:02 -07:00
Felix Rieseberg 4691c1b494 chore: Use a lockfile with latest npm 2019-10-27 11:10:36 -07:00
Felix Rieseberg 8125abc657 build: Update AppVeyor to Node 12 2019-10-27 11:01:39 -07:00
Felix Rieseberg 8cba7739f5 fix: Prepare for Electron 7 2019-10-25 11:59:05 -07:00
Felix Rieseberg 0b39fe40b1 chore: Update all the packages 2019-10-25 11:37:34 -07:00
Felix Rieseberg cae078ffe6 Merge pull request #266 from electron/ignored-commented-out-modules
fix: don't install commented-out requires
2019-10-15 18:14:33 -07:00
Ankit Arun bfb931435f fix: add whitespace to Welcome Tour (#263) 2019-10-14 20:48:40 -04:00
Shelley Vohr c878f72f2e fix: don't install commented-out requires 2019-10-14 13:59:00 -07:00
Felix Rieseberg 32494c057a Merge pull request #254 from electron/preload
feat: Enable support for a `preload.js`
2019-10-05 14:52:58 -10:00
Jason Zeng e732685fff fix: link to docs for content tracing show-me (#256) 2019-09-25 10:16:44 -04:00
Felix Rieseberg d9d5e0cbc9 test: Fix the tests 2019-09-17 16:14:38 -07:00
Felix Rieseberg 0e1d7b541e feat: If a file is newly empty, remove it 2019-09-17 15:59:27 -07:00
Felix Rieseberg 8a602d94b7 feat: Enable support for a preload.js 2019-09-17 15:50:49 -07:00
Erick Zhao 155889dbcc fix: ensure state consistency when replacing Fiddle (#253) 2019-09-16 14:53:53 -04:00
Felix Rieseberg 56ac6662b6 Merge pull request #248 from electron/add-new-themes
feat: ability to add new editor themes to Fiddle
2019-09-10 13:40:08 -04:00
Felix Rieseberg adb196549b chore: Bump version to 0.10 2019-09-08 15:45:59 -07:00
Felix Rieseberg 999facf062 Merge pull request #250 from electron/fix-console-stuff
fix: Console button / display issues
2019-09-08 15:39:55 -07:00
Felix Rieseberg fa0115f2db fix: Correctly toggle console state 2019-09-08 15:29:15 -07:00
Carlos Valdez c0a4c0e48d remove unused import 2019-09-07 11:05:13 -05:00
Carlos Valdez 3a5d2205fe remove unused imports 2019-09-07 11:03:27 -05:00
Carlos Valdez 1083b8f859 add warning dialog 2019-09-07 11:03:11 -05:00
Carlos Valdez 252d4c1de6 remove old code (yikes) 2019-09-07 10:25:10 -05:00
Carlos Valdez 0c44d2c9e4 remove check 2019-09-05 23:54:11 -05:00
Carlos Valdez 5eebc06514 add if statement inside try catch 2019-09-05 18:27:04 -05:00
Carlos Valdez eac1423177 remove comment 2019-09-05 18:18:44 -05:00
Carlos Valdez ae88780281 remove comment 2019-09-05 18:07:56 -05:00
Carlos Valdez 4bf9d48d70 remove commet 2019-09-05 18:07:36 -05:00
Carlos Valdez df6d4b5a9a remove monaco from observable variable name 2019-09-05 17:41:49 -05:00
Carlos Valdez 68202e2b23 change file name and change import 2019-09-05 17:23:40 -05:00
Felix Rieseberg acd5b35ca1 fix: Console button / display issues 2019-08-31 07:07:20 -07:00
Felix Rieseberg fae1ca3138 Merge pull request #249 from electron/update-dependencies
chore: Update all dependencies
2019-08-31 00:25:17 +02:00
Felix Rieseberg bd220d2690 chore: Update more dependencies 2019-08-30 15:07:07 -07:00
Felix Rieseberg d31f1016b8 test: Update all the tests 2019-08-30 15:05:07 -07:00
Felix Rieseberg 3b52be0f79 chore: Update templates for Standard 2019-08-28 09:44:10 +02:00
Felix Rieseberg 0e11c56f7f chore: Update all dependencies 2019-08-28 09:43:52 +02:00
Carlos Valdez 8019981d1a missing ; 2019-08-19 13:29:09 -07:00
Carlos Valdez 403a58e216 update snapshot 2019-08-19 13:28:42 -07:00
Carlos Valdez 3341d0ccaa fix json parsing 2019-08-19 11:13:32 -07:00
Carlos Valdez d42e83100a need to work on successful input 2019-08-16 17:58:27 -07:00
Carlos Valdez 50e9883d82 add new popup using react! 😰 2019-08-16 17:37:22 -07:00
Carlos Valdez f5aedac6a3 change wording of import monaco themes and add better formatting 2019-08-15 11:09:13 -07:00
Carlos Valdez a3f4aa8ed3 remove old code 2019-08-15 10:02:30 -07:00
Carlos Valdez 9c839aaad8 remove sad code 2019-08-15 09:57:30 -07:00
Carlos Valdez 3a7ca8ccd5 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-15 09:56:15 -07:00
Carlos Valdez fef42293bc account for user checking an incorrect file 2019-08-15 09:56:02 -07:00
Felix Rieseberg 1f9db42c80 Merge pull request #247 from electron/feat/local-version-intellisense
feat: intellisense for local builds
2019-08-14 14:51:18 -07:00
Carlos Valdez 8e5657ef2d the coolest recursive function thanks to Samuel 2019-08-14 13:34:43 -07:00
Carlos Valdez a94105dd27 the coolest recursive function thanks to Samuel 2019-08-14 13:34:29 -07:00
Carlos Valdez cf214dc0d0 remove bad method 2019-08-14 11:03:54 -07:00
Carlos Valdez 6dfdf00437 add in new function and button 2019-08-14 11:03:36 -07:00
Carlos Valdez b3743a4c73 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-13 17:07:15 -07:00
Carlos Valdez 3c0d6c2293 fix my mess 2019-08-13 17:06:23 -07:00
CarlosandErick Zhao 89b589fd2e Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:48 -07:00
CarlosandErick Zhao 42c577988d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:37 -07:00
CarlosandErick Zhao d0e47b6b4d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:34 -07:00
CarlosandErick Zhao f44a127012 Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:22 -07:00
Carlos Valdez fa2f5454ce don't throw! 2019-08-13 11:24:12 -07:00
Carlos Valdez 1021bb8b2b remove console.warn 2019-08-13 10:34:40 -07:00
Carlos Valdez cf4ad36ca2 remove optional arg 2019-08-13 10:33:38 -07:00
Carlos Valdez 4fe9c627b5 if json has name, set that as the filename otherwise take the filename 2019-08-13 10:25:21 -07:00
Carlos Valdez 262e636e8d update snapshot 2019-08-12 17:53:26 -07:00
Carlos Valdez bbe79ed6e2 added description 2019-08-12 15:32:46 -07:00
Carlos Valdez bc96a5013a added new VS Code option 2019-08-12 15:32:37 -07:00
Erick Zhao 66ba7b74d5 test: add spec 2019-08-12 10:13:08 -07:00
Erick Zhao 38e5aa98e3 test: remove '.only' 2019-08-09 13:06:28 -07:00
Erick Zhao a91504b173 feat: local typedefs 2019-08-09 13:05:07 -07:00
Erick Zhao 4685e88c86 style: run linter 2019-08-09 11:27:08 -07:00
Erick Zhao e0bb82d919 fix: throw appropriately if cannot fetch types 2019-08-08 15:43:14 -07:00
Erick Zhao 7035be3286 fix: adjust styling to avoid hiding intellisense (#243) 2019-07-30 10:43:44 -07:00
Erick Zhao c961cfadb8 chore: merge in settings UX improvements 2019-07-30 10:43:10 -07:00
Erick Zhao 7267a2ee88 fix: load empty files upon opening a fiddle (#241)
* fix: load empty files upon opening a fiddle
* test: add spec for no value passed to setValue
2019-07-30 10:42:16 -07:00
Erick Zhao bc03382dc5 test: test for event listener removal upon unmount 2019-07-29 08:48:59 -07:00
Erick Zhao 891c33ba68 fix: refresh theme list upon creation 2019-07-28 20:02:35 -07:00
Erick Zhao e7260c74a4 feat: close settings panel on Esc 2019-07-28 20:02:35 -07:00
Erick Zhao ed694a7044 chore: describe correct component in spec 2019-07-28 20:02:35 -07:00
Erick Zhao 14fc77c08c chore: 'Exection' -> 'Execution' 2019-07-28 20:02:35 -07:00
Erick Zhao f3079648cf fix: clean up typedefs when removing binaries (#240) 2019-07-26 18:03:57 -07:00
Felix Rieseberg 7272ec7899 Merge pull request #239 from electron/fix/consistent-tab-size
fix: enforce 2-space indents in all editors
2019-07-26 10:25:46 -07:00
Erick Zhao ac9ac701f7 test: add spec for forced tab size 2019-07-25 13:16:56 -07:00
Erick Zhao 7fce572823 fix: enforce tab-size 2 2019-07-25 12:40:53 -07:00
Erick Zhao 6186a80dad chore: 'monoaco' -> 'monaco' (#238) 2019-07-25 11:53:20 -07:00
Mark Lee ade6576285 build: upgrade Electron Forge to 6.0.0-beta.42 (#234) 2019-07-17 23:22:06 -07:00
Felix Rieseberg 61c58277f3 Merge pull request #233 from electron/chore/toogle
chore: `toogle` -> `toggle`
2019-07-17 19:48:00 -07:00
Erick Zhao c361d34a6d chore: toogle -> toggle 2019-07-16 17:54:02 -07:00
Erick Zhao 5887a6615a Merge pull request #232 from electron/feat/protocol-force-electron-version
feat: force electron version when loading from example protocol
2019-07-16 10:34:05 -07:00
Erick Zhao 7ba4cf5be7 style: run linter 2019-07-15 16:47:36 -07:00
Erick Zhao 0daf872d2b chore: conflict resolution w/ confirm prompt 2019-07-15 16:34:05 -07:00
Erick Zhao 75ec8ee9b1 chore: merge in master 2019-07-15 16:24:00 -07:00
Erick Zhao 183c152e40 test: create spec for remote-loader 2019-07-15 16:18:04 -07:00
Erick Zhao d6a807ba41 refactor: move version checking to its own fn 2019-07-15 15:23:30 -07:00
Carlos 24e0abf667 Merge pull request #230 from electron/warning-prompt
feat: Warning prompt
2019-07-15 14:05:37 -07:00
Carlos Valdez eb8261e240 remove obsolete snapshot 2019-07-15 13:38:20 -07:00
Erick Zhao 84cc07da90 feat: load examples with specific electron version 2019-07-15 13:21:44 -07:00
Erick Zhao b0f3837b47 refactor: move remote loading to its own file 2019-07-15 13:20:56 -07:00
Carlos Valdez e29664ca45 fix added event spec 2019-07-15 13:18:55 -07:00
Carlos Valdez ef36ee39f6 - 2019-07-15 11:14:56 -07:00
Carlos Valdez 73dadb815e removed spec test 2019-07-15 11:14:45 -07:00
Carlos Valdez 4945501703 fix test spec 2019-07-15 11:02:08 -07:00
Erick Zhao 77c87fbdeb feat: console resizing (#226)
Adds a `react-mosaic` wrapper to the console component to enable resizing in a similar
fashion to the editor components.
2019-07-15 10:33:01 -07:00
Carlos Valdez 2ef5b42f4a solved conflicts on git rebase 2019-07-15 10:31:07 -07:00
Carlos Valdez 9c4bc2ba52 fix test-spec failing 2019-07-15 10:30:31 -07:00
Carlos Valdez 80da4e73e3 fixed specs 2019-07-15 10:30:31 -07:00
Carlos Valdez eadd58d149 fix test 2019-07-15 10:28:09 -07:00
Carlos Valdez 1fc82f8f6d update specs 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 9bd7043b2c Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos aaeef6fd06 Update commands-address-bar.tsx 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 1195902bd5 Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg c022d0f33d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 996ae775f3 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos Valdez ed44dda8fb better label 2019-07-15 10:28:09 -07:00
Carlos Valdez effba13e73 add to dialog class 2019-07-15 10:28:09 -07:00
Carlos Valdez 65886c3b52 added dialog-confirm prompt for users 2019-07-15 10:28:09 -07:00
Carlos Valdez 8bd4dd366a fix test-spec failing 2019-07-15 10:16:46 -07:00
Erick Zhao 4e19578e34 feat: add support for unchecked release channels 2019-07-14 18:45:18 -07:00
Erick Zhao c7fd0bc95e feat: force electron version when load w/ protocol 2019-07-14 18:45:18 -07:00
Erick Zhao 29e2f03b1a feat: allow version utility fn to accept a string 2019-07-14 18:45:18 -07:00
Felix Rieseberg 4cd6f330da Merge pull request #227 from electron/deermichel/error-msg
chore: add offline notice when loading fiddles
2019-07-13 16:10:27 -07:00
Carlos Valdez 79521eac7c fixed specs 2019-07-12 16:03:28 -07:00
Carlos Valdez 2736499d68 fix test 2019-07-12 15:36:14 -07:00
Carlos Valdez 1cbb781e8d update specs 2019-07-12 15:25:02 -07:00
CarlosandFelix Rieseberg e849a49f6f Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:48:40 -07:00
Carlos 0114d2c1eb Update commands-address-bar.tsx 2019-07-12 14:48:09 -07:00
CarlosandFelix Rieseberg 365537012f Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:45:00 -07:00
CarlosandFelix Rieseberg 3e435cb64d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:44:55 -07:00
CarlosandFelix Rieseberg 6981382793 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:42:53 -07:00
Felix Rieseberg f293ba84d1 Merge pull request #229 from electron/feat/protocol-dev-mode-support
feat: add dev mode support for protocol
2019-07-12 14:41:05 -07:00
Carlos Valdez 55cf507a96 better label 2019-07-12 14:23:21 -07:00
Carlos Valdez e71c2439cd add to dialog class 2019-07-12 13:49:52 -07:00
Carlos Valdez 9bd253951c added dialog-confirm prompt for users 2019-07-12 13:49:40 -07:00
Erick Zhao eda08c9aea feat: change args passed in to support yarn
NPM and Yarn both forward arguments via `process.env.npm_config_argv`.
The URL would be in the `remain` property in NPM, but in `original`
in both NPM and Yarn.
2019-07-12 11:50:35 -07:00
Felix Rieseberg 9414f66893 Merge pull request #228 from electron/no-prerelease
build: Don't mark releases as prereleases
2019-07-12 11:44:40 -07:00
Erick Zhao f4725b5dd8 feat: add dev mode support for protocol 2019-07-12 11:35:18 -07:00
Felix Rieseberg fd8be47494 build: Don't mark releases as prereleases 2019-07-12 11:10:09 -07:00
Micha Hanselmann 2cebe2ca7c add more verbose msg when offline 2019-07-12 11:07:51 -07:00
Felix Rieseberg 74135bab67 0.9.0 2019-07-08 10:53:09 -07:00
Felix Rieseberg a856340d01 Merge pull request #225 from electron/update-dependencies
chore: Update the dependencies
2019-07-08 10:52:25 -07:00
Felix Rieseberg bd8939c358 chore: Update the dependencies 2019-07-07 17:15:43 -07:00
Felix Rieseberg 9d3f3d61eb Merge pull request #222 from erickzhao/fix/browserwindow-showme-script
fix: correct button code for BrowserWindow show-me
2019-07-06 12:46:07 -07:00
Felix Rieseberg bdeaffcb7d Merge pull request #223 from DeerMichel/deermichel/nightlies
fix: fetch nightlies from `electron-nightlies` channel
2019-07-06 12:45:40 -07:00
Andrew Cherniavskii 344e1df375 fix: console message in powermonitor example 2019-07-06 09:34:03 -07:00
Micha Hanselmann d5b52dcae7 fix nightlies fetch 2019-07-05 13:48:07 -07:00
Erick Zhao 0faeb24761 fix: correct button code for BrowserWindow show-me 2019-07-03 15:23:07 -07:00
Micha Hanselmann 284b579d0c fix out-of-scope tray ref (#221) 2019-07-03 11:14:39 -05:00
Felix Rieseberg 5a1c24cdda Merge pull request #220 from erickzhao/feat/publish-ux-fixes
feat: clarify UX for Publish Gist feature
2019-07-02 10:13:02 -07:00
Erick Zhao 024d48a273 test: add spec for publishing gist disabled form 2019-07-01 22:54:44 -07:00
Erick Zhao 32155d652d chore: update enzyme version 2019-07-01 22:54:04 -07:00
Erick Zhao 874a8bb0f0 test: Move isPublishing from local state to store 2019-07-01 21:15:18 -07:00
Erick Zhao 73470640c7 test: Update snapshots to match DOM changes 2019-07-01 21:15:18 -07:00
Erick Zhao 679f731f4c chore: Consistent use of "publish" keyword 2019-07-01 21:15:18 -07:00
Erick Zhao a41ad11f8c chore: Remove "public" from gist setting callout
Users can either set their published Gists as public or private, so it's
best to remove the "public" qualifier from the callout.
2019-07-01 21:15:18 -07:00
Erick Zhao 7cc6d38dc2 feat: Disable fields while publishing gists 2019-07-01 21:15:18 -07:00
Felix Rieseberg 1e6ee7ab37 Merge pull request #216 from drewswaycool/add-missing-spaces
adds missing spaces
2019-06-27 15:43:25 -07:00
Felix Rieseberg c50c31ad80 Merge pull request #219 from electron/bypass-contributor-check
build: Don't throw if missing contributors in PR
2019-06-27 15:35:20 -07:00
Felix Rieseberg 5f8557fb13 build: Don't throw if missing contributors in PR 2019-06-24 19:51:34 -07:00
Felix Rieseberg 47b20fa3e4 Merge pull request #218 from DeerMichel/deermichel/clear-console
feat: add ability to clear console output
2019-06-24 19:47:21 -07:00
Micha Hanselmann b5c8ed60a9 remove clear console top bar button 2019-06-17 21:16:23 -07:00
Micha Hanselmann 5e3d382b85 minor style fix 2019-06-17 09:41:57 -07:00
Micha Hanselmann fed0107dd1 i'm a contributer now... 2019-06-16 00:11:31 -07:00
Micha Hanselmann ca9b9c633a add clear-on-run test 2019-06-15 23:50:36 -07:00
Micha Hanselmann fb4c6b3f33 add ui tests for console settings 2019-06-15 23:38:29 -07:00
Micha Hanselmann aa51c0cc5c implement clear on run 2019-06-15 23:28:20 -07:00
Micha Hanselmann e5236c5500 add settings ui for clear-on-run option 2019-06-15 23:22:49 -07:00
Micha Hanselmann 321f7a8bbc fix top bar buttons test 2019-06-15 23:02:28 -07:00
Micha Hanselmann 1ff7a3fe16 fix context menu test 2019-06-15 23:00:04 -07:00
Micha Hanselmann a1c74c018d add test for clearConsole 2019-06-15 22:53:48 -07:00
Micha Hanselmann 5df39ea51a add context menu entry to clear console 2019-06-15 22:41:44 -07:00
Micha Hanselmann 8a9591be9f add clear console button to top bar 2019-06-15 22:37:25 -07:00
Micha Hanselmann 3cc8fd41f6 add clearConsole method 2019-06-15 22:29:32 -07:00
Drew McMurry 25df3dd989 adds missing spaces 2019-06-14 15:38:39 -06:00
Felix Rieseberg abe7bdab23 Merge pull request #213 from electron/fix-touch-bar-example
fix: correct touch bar constructor in example
2019-06-11 21:16:44 -04:00
Shelley Vohr 16bea01ba5 fix: correct touch bar constructor in example 2019-06-11 14:17:44 -07:00
Felix Rieseberg 7417a82931 0.8.1 2019-06-11 11:35:40 -04:00
Felix Rieseberg 4c41db8595 Merge pull request #212 from electron/no-download-test
test: Mock electron-download
2019-06-11 17:35:23 +02:00
Felix Rieseberg 5da87ffca3 Merge pull request #211 from electron/fix-menu
fix: Don't attempt to set about panel on Windows
2019-06-11 17:05:07 +02:00
Felix Rieseberg 919e9cb6b6 test: Mock electron-download 2019-06-11 11:02:09 -04:00
Felix Rieseberg 14279283d6 fix: Don't attempt to set about panel on Windows 2019-06-11 10:51:42 -04:00
Felix Rieseberg 77a98112a1 Merge pull request #209 from amorist/fix-submit-token
Fix: submit token
2019-06-11 16:43:05 +02:00
amorist 08fb3f52bf fixed submit token 2019-06-11 14:27:47 +08:00
Felix Rieseberg b37ba895e5 0.8.0 2019-06-10 12:09:48 -04:00
Felix Rieseberg 33a236edd1 Merge pull request #203 from electron/load-electron-examples
feat: add support for launch examples from the Electron docs
2019-06-07 21:30:27 +02:00
Felix Rieseberg f7930fb523 test: Fix more tests 2019-06-07 12:15:49 -07:00
Felix Rieseberg 88cad66076 test: Add some tests 2019-06-07 11:01:36 -07:00
Felix Rieseberg f0afedf896 fix: Linter issues 2019-06-07 09:39:36 -07:00
Samuel Attard 38234dd0a3 chore: fix specs 2019-06-06 14:59:53 -07:00
Felix Rieseberg 83e834bea5 Merge pull request #205 from electron/touchbar
feat: Touchbar
2019-06-06 18:23:18 +02:00
Felix Rieseberg 38be3cca14 test: Add touchBarManager creation 2019-06-03 15:59:00 -07:00
Felix Rieseberg 4908cc6110 feat: Sort versions in touch bar 2019-06-03 13:11:11 -07:00
Felix Rieseberg ab4a9015d1 feat: Add a touchbar feature 2019-06-02 21:12:34 -07:00
Felix Rieseberg 73ab813efc Merge pull request #204 from electron/add-about-panel
feat: add more information to about panel
2019-06-02 09:30:00 +02:00
Shelley Vohr f3570c5069 spec: add a spec for about panel functionality 2019-06-01 09:54:25 -07:00
Shelley Vohr 4c564eaa63 feat: add more information to about panel 2019-05-31 22:32:05 -07:00
Samuel Attard a289842e32 feat: add support for launch examples from the Electron docs 2019-05-27 11:03:18 -07:00
Felix Rieseberg 9d697e7a3f Merge pull request #202 from electron/gist-launch
feat: add wiring for protocol launching gists
2019-05-27 00:16:51 -07:00
Felix Rieseberg e96e5345e6 Merge pull request #201 from electron/fix-audit
chore: fix npm audit output
2019-05-27 00:16:00 -07:00
Samuel Attard 7c97bc2ef6 feat: add wiring for protocol launching gists 2019-05-25 15:29:04 -07:00
Samuel Attard d55c5c067d chore: fix npm audit output 2019-05-25 14:08:14 -07:00
Felix Rieseberg 083ac3fbc2 chore: Update monaco-loader 2019-05-17 17:23:47 +09:00
Felix Rieseberg 5c8fe3d6e0 build: Fix x64 releases 2019-05-17 14:00:54 +09:00
Felix Rieseberg f034c5240c chore: Update version number 2019-05-17 13:29:40 +09:00
Felix Rieseberg 6421187db4 Merge pull request #197 from electron/editors-to-panels
feat: Panels
2019-05-17 13:00:26 +09:00
Felix Rieseberg 6fb385a95f fix: Import typo 2019-05-17 12:39:14 +09:00
Felix Rieseberg c47fea09ed fix: Don't whine plz 2019-05-17 12:31:10 +09:00
Felix Rieseberg ef17ccd9cb test: Update the tests 2019-05-17 12:31:10 +09:00
Felix Rieseberg 3f173aa4d4 chore: Better panel titles 2019-05-17 12:31:10 +09:00
Felix Rieseberg faf15081e0 fix: Fix linter issues 2019-05-17 12:31:10 +09:00
Felix Rieseberg 4306f53c70 feat: A first demo 2019-05-17 12:31:10 +09:00
Felix Rieseberg 52a8aac6c7 test: Add a test for the default show-me page 2019-05-17 12:31:10 +09:00
Felix Rieseberg 7660192dcb test: Update the tests 2019-05-17 12:31:10 +09:00
Felix Rieseberg 53ceeb5600 feat: New overview button for docs & demos 2019-05-17 12:31:10 +09:00
Felix Rieseberg 5cd7a57f08 feat: Basic implementation of demo pages 2019-05-17 12:31:10 +09:00
Felix Rieseberg e96a3da09b feat: A generic "this is a subset only" component 2019-05-17 12:31:10 +09:00
Felix Rieseberg 90c198d487 feat: A generic "more information" component 2019-05-17 12:31:10 +09:00
Felix Rieseberg 5b56333516 feat: A utility to get docs urls 2019-05-17 12:31:10 +09:00
Felix Rieseberg c87f65ca76 feat: A generic show me component 2019-05-17 12:31:10 +09:00
Felix Rieseberg 0f27c8aa7a fix: Typo in type-checks 2019-05-17 12:31:10 +09:00
Felix Rieseberg b5880aef46 feat: Give docs their own little button 2019-05-17 12:31:10 +09:00
Felix Rieseberg fb46eaf0e4 feat: Improve panel styles 2019-05-17 12:31:10 +09:00
Felix Rieseberg c1040097e3 fix: A small issue with panel visibility 2019-05-17 12:31:10 +09:00
Felix Rieseberg 333758b73d feat: Basic awareness of opened template 2019-05-17 12:31:10 +09:00
Felix Rieseberg 3c2a4e1b76 test: Improve the tests, cover more 2019-05-17 12:31:10 +09:00
Felix Rieseberg f3d42c107f test: Add test for type check 2019-05-17 12:31:10 +09:00
Felix Rieseberg 2ece4dc728 feat: Editors to Panels, for real 2019-05-17 12:31:10 +09:00
Felix Rieseberg dd2975bd0e feat: Add the ability to render generic panels 2019-05-17 12:31:10 +09:00
Felix Rieseberg fdf03dff9b Merge pull request #198 from electron/updates
feat: Update all dependencies (Electron 5)
2019-05-17 12:30:44 +09:00
Felix Rieseberg 48fa762f7f build: Fine, Node v10 it is >:( 2019-05-17 12:16:26 +09:00
Felix Rieseberg 7cd9a86920 fix: Appease AppVeyor 2019-05-17 12:12:30 +09:00
Felix Rieseberg db2caaad59 feat: Update all dependencies (Electron 5) 2019-05-17 12:09:49 +09:00
Felix Rieseberg 3b2cb93d8b Merge pull request #195 from ajphukan/require-not-defined-bug
Fix: Require not defined bug
2019-05-16 10:58:49 +09:00
Ankur Jyoti Phukan 9822753ffd Update contributors.js 2019-05-09 12:54:48 +05:30
Ankur Jyoti Phukan 8712907182 Removed trailing space 2019-05-09 12:40:00 +05:30
Ankur Jyoti Phukan 40055ee069 Removed trailing space 2019-05-09 12:38:53 +05:30
Ankur Jyoti Phukan a33f175867 Removed trailing space 2019-05-09 12:38:11 +05:30
Ankur Jyoti Phukan 107ba63a5a Removed trailing spcae 2019-05-09 12:36:56 +05:30
Ankur Jyoti Phukan b66ad7e247 Removed trailing space 2019-05-09 12:36:20 +05:30
Ankur Jyoti Phukan 7eb15b6666 Removed Trailing space 2019-05-09 12:35:43 +05:30
Ankur Jyoti Phukan 2ed29f68dd Update main.js 2019-05-09 12:34:46 +05:30
Ankur Jyoti Phukan d89d595b57 Removed Trailing Space 2019-05-09 12:29:07 +05:30
Ankur Jyoti Phukan 2b34ef5e67 Update main.js 2019-05-09 12:23:08 +05:30
Ankur Jyoti Phukan d6f7146447 Update main.js 2019-05-09 12:22:32 +05:30
Ankur Jyoti Phukan 006ba5f0a5 Update main.js 2019-05-09 12:21:55 +05:30
Ankur Jyoti Phukan 5df390b0fd Update main.js 2019-05-09 12:21:13 +05:30
Ankur Jyoti Phukan 3eb25fed91 Update main.js 2019-05-09 12:20:21 +05:30
Ankur Jyoti Phukan 9b06f792ea Update main.js 2019-05-09 12:19:50 +05:30
Ankur Jyoti Phukan c116152605 Update main.js 2019-05-09 12:19:33 +05:30
Ankur Jyoti Phukan b9f8d55809 Update main.js 2019-05-09 12:19:13 +05:30
Ankur Jyoti Phukan 89e697fcd6 Update main.js 2019-05-09 12:18:53 +05:30
Ankur Jyoti Phukan 9df5314c08 Update main.js 2019-05-09 12:17:43 +05:30
Ankur Jyoti Phukan d01ba1cdff Added webPreference 2019-05-09 12:01:19 +05:30
Ankur Jyoti Phukan ed5ccf133f Added webPreference 2019-05-09 12:00:28 +05:30
Ankur Jyoti Phukan e1decd4819 Added webPreference 2019-05-09 11:59:49 +05:30
Ankur Jyoti Phukan 3c9f477b89 Added webPreference 2019-05-09 11:57:22 +05:30
Ankur Jyoti Phukan 5cfbf2cab5 Added webPreference 2019-05-09 11:56:36 +05:30
Ankur Jyoti Phukan 89272fbaa9 Added webPreference 2019-05-09 11:55:47 +05:30
Ankur Jyoti Phukan 5d572f451c Added webPreference 2019-05-09 11:54:52 +05:30
Ankur Jyoti Phukan 319136ecc8 Added WebPreference 2019-05-09 11:51:08 +05:30
Ankur Jyoti Phukan 3a4aa06331 Added WebPreference 2019-05-09 11:48:17 +05:30
Felix Rieseberg e283a118f6 Merge pull request #187 from malept/update-forge-transform
fix: use the latest version of Forge v6 for Fiddle Forge actions
2019-04-12 11:44:24 -07:00
Mark Lee e67d1b0179 fix: use the latest version of Forge v6 for Fiddle Forge actions 2019-04-10 23:28:42 -07:00
Felix Rieseberg 9805d98dd8 Merge pull request #184 from electron/test-100
test: Ensure full coverage
2019-04-02 16:11:20 -07:00
Felix Rieseberg 009ad25983 test: Test GitHub authentication in address bar 2019-04-02 15:52:23 -07:00
Felix Rieseberg 9a65124289 test: Test editors with visible editors 2019-04-02 15:39:44 -07:00
Felix Rieseberg 4d7c81ee69 Merge pull request #183 from electron/panel-management
feat: Panel management (close and open editors)
2019-04-02 15:27:06 -07:00
Felix Rieseberg ed91cb5ba1 test: Increase test coverage 2019-04-01 16:23:17 -07:00
Felix Rieseberg 8b04f67f71 fix: Linting issues 2019-04-01 16:15:34 -07:00
Felix Rieseberg 2e183f0538 fix: Handle setting values on closed editors 2019-04-01 16:13:51 -07:00
Felix Rieseberg 6b022bbea7 test: Improve the tests 2019-04-01 15:54:03 -07:00
Felix Rieseberg 6bdb7d71ec feat: Add a non-ideal state for the editors 2019-03-31 17:31:22 -07:00
Felix Rieseberg 0ebe6ab9b3 fix: Style when there is only one panel 2019-03-31 16:24:05 -07:00
Felix Rieseberg 5e73deeb4f test: Polish and lint tests 2019-03-31 14:04:24 -07:00
Felix Rieseberg 02d81aa407 test: Add spec for the new state methods 2019-03-31 13:59:20 -07:00
Felix Rieseberg 46deeb857e test: Test editors-mosaic-arrangement 2019-03-31 13:35:34 -07:00
Felix Rieseberg ca55d2e620 test: Add a spec for editors-toolbar-button 2019-03-31 13:07:36 -07:00
Felix Rieseberg 88974d277f test: Update the editor spec 2019-03-31 13:01:06 -07:00
Felix Rieseberg 1239afafeb test: Add spec for commands-editor 2019-03-31 12:45:36 -07:00
Felix Rieseberg 415877c067 test: Add spec for devtools 2019-03-31 12:24:54 -07:00
Felix Rieseberg 0a0779ffa1 test: Add tests for the new editor utilities 2019-03-31 12:17:46 -07:00
Felix Rieseberg 9cf34ea983 test: Fix the existing tests 2019-03-31 12:06:04 -07:00
Felix Rieseberg 83a7d23b38 test: Fix Wallaby & Jest 2019-03-31 12:00:09 -07:00
Felix Rieseberg 7d19ad91a6 feat: Add panel management 2019-03-31 10:39:56 -07:00
Felix Rieseberg db13699790 feat: Install dev tools 2019-03-31 10:36:55 -07:00
Felix Rieseberg 4a8fc24ff6 fix: Version number test 2019-03-30 17:02:39 -07:00
Felix Rieseberg 3ed41626b1 chore: Update version, electron versions 2019-03-28 09:29:44 -07:00
Felix Rieseberg b3e25cfe79 Merge pull request #182 from electron/electron-logging
feat: add an execution toggle to enable advanced electron logging
2019-03-27 15:15:49 -07:00
Samuel Attard 00bb15a891 feat: add an execution toggle to enable advanced electron logging 2019-03-27 11:02:12 -07:00
Felix Rieseberg 94da51128a Merge pull request #181 from electron/update-dependencies
feat: Update all dependencies
2019-03-25 14:51:43 -04:00
Felix Rieseberg b573f09494 fix: Lockfile was somehow out of sync 2019-03-25 10:19:51 -07:00
Felix Rieseberg f05e36de34 infra: Update all dependencies 2019-03-25 10:17:14 -07:00
Felix Rieseberg 44c7c70793 Merge pull request #178 from electron/bump-version-06
build: Update the version number
2019-02-22 11:54:49 +00:00
Felix Rieseberg bcbca7aceb build: Update the version number 2019-02-22 11:54:34 +00:00
Felix Rieseberg 9515458545 Merge pull request #177 from electron/electron-405
build: Update to Electron 4.0.5
2019-02-22 11:53:45 +00:00
Felix Rieseberg 219e90cad8 build: Update to Electron 4.0.5 2019-02-22 11:44:27 +00:00
Felix Rieseberg ba23c8019d Merge pull request #175 from electron/update-octokit
feat: Update octokit, handle fiddle load errors
2019-02-22 11:42:18 +00:00
Felix Rieseberg 2fef96a2b2 Merge branch 'master' into update-octokit 2019-02-22 11:31:15 +00:00
Felix Rieseberg 2d8cb9748d Merge pull request #176 from electron/update-dependencies-2
build: Update all the dependencies
2019-02-22 11:30:38 +00:00
Felix Rieseberg eaefbb7ad0 infra: Update all the dependencies 2019-02-21 11:32:11 +00:00
Felix Rieseberg d5f28253f8 feat: Handle loadFiddle() errors 2019-02-21 11:15:50 +00:00
Felix Rieseberg ac451d6b4a infra: Update Octokit methods 2019-02-21 11:01:03 +00:00
Felix Rieseberg b5e0e63766 infra: Update Octokit 2019-02-21 11:00:50 +00:00
Felix Rieseberg 49538ea55a Merge pull request #174 from electron/fix-show-me
fix: Lower-case names for show-me-templates
2019-02-21 10:26:16 +00:00
Felix Rieseberg 23ac9b8e02 fix: Lower-case names for show-me-templates 2019-02-20 16:27:47 +00:00
Felix Rieseberg 4bf2bee7c8 docs: Update the readme with new screenshot 2019-02-01 16:44:58 -08:00
Felix Rieseberg e742d8426c Merge pull request #172 from electron/fix-contributors-script
fix: Allow contributors.js to use GitHub credentials
2019-02-01 15:54:38 -08:00
Felix Rieseberg 7a223fd3dc fix: Allow contributors.js to use GitHub credentials 2019-02-01 15:14:41 -08:00
Felix Rieseberg 6bdc995eb6 Merge pull request #170 from electron/feat-158
feat: Display only Electron versions with a certain state
2019-01-30 22:07:35 -08:00
Felix Rieseberg 72cbcd46d1 test: Update one last test 2019-01-30 21:55:18 -08:00
Felix Rieseberg f1e51c71a3 Merge pull request #171 from electron/properly-quit
fix: On quit, quit properly
2019-01-29 19:01:28 -08:00
Felix Rieseberg 95e88c0eee test: Fix tests 2019-01-27 00:25:26 -08:00
Felix Rieseberg a54037ef0e test: Update snapshot 2019-01-26 19:15:26 -08:00
Felix Rieseberg 83bb6fc73e test: Add more tests, 2019-01-26 19:10:15 -08:00
Felix Rieseberg f8f1f0ec08 fix: On quit, quit properly 2019-01-26 18:59:40 -08:00
Felix Rieseberg f7a19b92a8 Merge pull request #169 from electron/more-enoent-logging
fix: Add some detailed templates logging
2019-01-26 18:39:55 -08:00
Felix Rieseberg dc76b78337 feat: Include it in the version chooser 2019-01-26 18:39:28 -08:00
Felix Rieseberg f6f3e94a97 feat: Add a setting for states to show 2019-01-26 18:36:34 -08:00
Felix Rieseberg 89cede25b1 Merge pull request #168 from electron/fix-167
fix: Correctly load Fiddle without form submit
2019-01-26 18:15:12 -08:00
Felix Rieseberg 8e8eec3cc6 fix: Add some detailed templates logging 2019-01-26 18:14:15 -08:00
Felix Rieseberg a0d8ca4019 fix: Correctly load Fiddle without form submit 2019-01-26 17:58:42 -08:00
Felix Rieseberg 1f0581afbc Merge pull request #166 from electron/bump-version-050
chore: Bump version to 0.5.0
2019-01-23 12:44:07 -08:00
Felix Rieseberg 70d4902b87 chore: Bump version to 0.5.0 2019-01-22 17:37:12 -08:00
Felix Rieseberg a93a99e3f0 Merge pull request #165 from electron/electron-4
feat: Update to Electron 4
2019-01-22 17:32:10 -08:00
Felix Rieseberg 4c209159e6 chore: jk, 4.0.2 2019-01-22 17:21:34 -08:00
Felix Rieseberg b09a248c96 feat: Update to Electron 4 2019-01-22 17:04:29 -08:00
Felix Rieseberg 6f09ed9c2a Merge pull request #163 from electron/forge-6-b32
chore: update Electron Forge to 6.0.0-beta.32
2019-01-21 12:51:56 -08:00
Felix Rieseberg c1ae0ecd86 Merge pull request #162 from electron/miniak/node-integration
feat: support upcoming Electron 5.0 with nodeIntegration disabled by default
2019-01-21 12:51:17 -08:00
Mark Lee 1e9a81fcf4 chore: update Electron Forge to 6.0.0-beta.32 2019-01-16 16:37:40 -08:00
Milan Burda 2061b26ed6 feat: support upcoming Electron 5.0 with nodeIntegration disabled by default 2019-01-16 20:10:52 +01:00
Felix Rieseberg 88ce4a76e9 Merge pull request #160 from bpasero/bpasero-run-f5
feat: Allow to run a fiddle via F5
2019-01-11 14:40:49 -08:00
Benjamin Pasero 9b4ef4c0d2 Allow to run a fiddle via F5
Enables to run a fiddle just with the keyboard.
2019-01-11 11:50:13 +01:00
Felix Rieseberg dca2cd67e5 Merge pull request #156 from electron/mac-better-frame
feat: support double click action on custom mac window title
2019-01-10 14:18:56 -08:00
Samuel Attard 6bab353088 fix: properly animate the opening / closing of the terminal 2019-01-09 13:44:10 +13:00
Samuel Attard e6cc44ca37 feat: support double click action on the window title bar on macOS 2019-01-09 13:43:55 +13:00
323 changed files with 17334 additions and 9392 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
language: node_js
node_js: "8"
node_js: "12"
os:
- linux
- osx
dist: trusty
osx_image: xcode8.3
osx_image: xcode10
sudo: false
cache:
+5 -5
View File
@@ -5,8 +5,8 @@
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,
choose the version of Electron you want to run it with, and play around. Then,
save your Fiddle either as a GitHub Gist or to a local folder. Once pushed to
GitHub, anyone can quickly try your Fiddle out by just entering it in the
save your Fiddle either as a GitHub Gist or to a local folder. Once published
on GitHub, anyone can quickly try your Fiddle out by just entering it in the
address bar.
<h3 align="center">
@@ -15,13 +15,13 @@ address bar.
</a>
</h3>
<img src="https://user-images.githubusercontent.com/378023/49716908-7d2b0500-fc98-11e8-8f52-def504802509.png" width="880px" alt="Electron Fiddle screenshot">
<img src="https://user-images.githubusercontent.com/1426799/52155868-d3357c80-2639-11e9-9496-fa97b1dc7897.jpg" width="880px" alt="Electron Fiddle screenshot">
# Features
### Explore Electron
![Screenshot: Electron App running](https://user-images.githubusercontent.com/1426799/43873856-5f66e56e-9b3d-11e8-8472-3a14d6a08c62.png)
![Screenshot: Electron App running](https://user-images.githubusercontent.com/1426799/52155856-c0bb4300-2639-11e9-9962-a6354d08dc5a.jpg)
Try Electron without installing any dependencies: Fiddle includes everything
you'll need to explore the platform. It also includes examples for every API
@@ -40,7 +40,7 @@ all Electron APIs only a few keystrokes away.
### Compile and Package
![Screenshot: Fiddle's Tasks Menu](https://user-images.githubusercontent.com/1426799/43874349-3f5abd74-9b40-11e8-9225-ddd1f1087a47.png)
![Screenshot: Fiddle's Tasks Menu](https://user-images.githubusercontent.com/1426799/52155857-c0bb4300-2639-11e9-8776-e05dc528439c.png)
Fiddle can automatically turn your experiment into binaries you can share with
your friends, coworkers, or grandparents. It does so thanks to
+1 -1
View File
@@ -1,6 +1,6 @@
environment:
matrix:
- nodejs_version: "10"
- nodejs_version: "12"
init:
- git config --global core.symlinks true
+36 -3
View File
@@ -7,7 +7,7 @@ const packageJson = require('./package.json')
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
module.exports = {
const config = {
hooks: {
generateAssets: require('./tools/generateAssets')
},
@@ -32,7 +32,12 @@ module.exports = {
OriginalFilename: 'Electron Fiddle',
},
osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
'entitlements': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library'
}
},
makers: [
@@ -90,8 +95,36 @@ module.exports = {
name: 'fiddle'
},
draft: true,
prerelease: true
prerelease: false
}
}
]
}
function notarizeMaybe() {
if (process.platform !== 'darwin') {
return;
}
if (!process.env.CI) {
console.log(`Not in CI, skipping notarization`);
return;
}
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!');
return;
}
config.packagerConfig.osxNotarize = {
appBundleId: 'com.electron.fiddle',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ'
}
}
notarizeMaybe()
// Finally, export it
module.exports = config
+3 -1
View File
@@ -7,7 +7,9 @@
"bail": true,
"resetMocks": true,
"resetModules": true,
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js",
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.js"
],
"moduleFileExtensions": [
"js",
"jsx",
+8040 -6346
View File
File diff suppressed because it is too large Load Diff
+61 -59
View File
@@ -1,7 +1,7 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.4.2",
"version": "0.11.1",
"description": "The easiest way to get started with Electron",
"repository": "https://github.com/electron/fiddle",
"main": "./dist/src/main/main",
@@ -11,7 +11,7 @@
"lint:style": "stylelint ./src/less/*.less --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/templates/**/*.js",
"lint:templates": "standard ./static/show-me/**/*.js",
"lint": "npm-run-all lint:*",
"make": "electron-forge make",
"package": "electron-forge package",
@@ -36,71 +36,73 @@
"forge": "./forge.config.js"
},
"dependencies": {
"@blueprintjs/core": "^3.10.0",
"@blueprintjs/select": "^3.4.0",
"@octokit/rest": "^15.12.1",
"builtin-modules": "^3.0.0",
"@blueprintjs/core": "^3.19.1",
"@blueprintjs/select": "^3.11.1",
"@octokit/rest": "^16.34.0",
"builtin-modules": "^3.1.0",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.1",
"electron-devtools-installer": "^2.2.4",
"electron-download": "^4.1.1",
"electron-squirrel-startup": "^1.0.0",
"extract-zip": "^1.6.7",
"fix-path": "^2.1.0",
"fs-extra": "^7.0.1",
"mobx": "^5.8.0",
"mobx-react": "^5.4.3",
"monaco-loader": "^0.15.0",
"namor": "^1.1.1",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-mosaic-component": "^2.0.2",
"semver": "^5.6.0",
"tmp": "0.0.33",
"tslib": "^1.9.3",
"update-electron-app": "^1.3.0"
"fs-extra": "^8.1.0",
"mobx": "^5.14.2",
"mobx-react": "^6.1.4",
"monaco-editor": "^0.18.1",
"monaco-loader": "1.0.0",
"namor": "^1.1.3",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-mosaic-component": "^3.2.0",
"semver": "^6.3.0",
"tmp": "0.1.0",
"tslib": "^1.10.0",
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.30",
"@electron-forge/maker-deb": "^6.0.0-beta.30",
"@electron-forge/maker-rpm": "^6.0.0-beta.30",
"@electron-forge/maker-squirrel": "^6.0.0-beta.30",
"@electron-forge/maker-zip": "^6.0.0-beta.30",
"@electron-forge/publisher-github": "^6.0.0-beta.30",
"@types/builtin-modules": "^2.0.0",
"@types/classnames": "^2.2.7",
"@types/enzyme": "^3.1.13",
"@types/fs-extra": "^5.0.4",
"@types/jest": "^23.3.2",
"@types/log-symbols": "^2.0.0",
"@types/node": "^10.11.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/semver": "^5.5.0",
"@types/tmp": "0.0.33",
"chokidar": "^2.0.4",
"coveralls": "^3.0.2",
"electron": "3.0.14",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.5",
"jest": "^23.6.0",
"jest-fetch-mock": "^1.6.6",
"less": "^3.8.1",
"log-symbols": "^2.2.0",
"monaco-editor": "^0.15.6",
"node-abi": "^2.5.1",
"node-fetch": "^2.3.0",
"@babel/core": "^7.6.4",
"@electron-forge/cli": "^6.0.0-beta.46",
"@electron-forge/maker-deb": "^6.0.0-beta.46",
"@electron-forge/maker-rpm": "^6.0.0-beta.46",
"@electron-forge/maker-squirrel": "^6.0.0-beta.46",
"@electron-forge/maker-zip": "^6.0.0-beta.46",
"@electron-forge/publisher-github": "^6.0.0-beta.46",
"@types/builtin-modules": "^3.1.1",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.3",
"@types/fs-extra": "^8.0.1",
"@types/jest": "^24.0.19",
"@types/log-symbols": "^3.0.0",
"@types/node": "^12.11.7",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/semver": "^6.0.2",
"@types/tmp": "0.1.0",
"chokidar": "^3.2.2",
"coveralls": "^3.0.7",
"electron": "7.1.3",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.4.3",
"jest": "^24.9.0",
"jest-fetch-mock": "^2.1.2",
"less": "^3.10.3",
"log-symbols": "^3.0.0",
"node-abi": "^2.12.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.11.0",
"react-test-renderer": "^16.7.0",
"rimraf": "^2.6.3",
"standard": "^12.0.1",
"stylelint": "^9.9.0",
"stylelint-config-standard": "^18.2.0",
"ts-jest": "^23.10.5",
"tslint": "^5.12.0",
"tslint-microsoft-contrib": "^6.0.0",
"tslint-react": "^3.6.0",
"typescript": "^3.2.2"
"parcel-bundler": "^1.12.4",
"react-test-renderer": "^16.11.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^11.1.1",
"stylelint-config-standard": "^19.0.0",
"ts-jest": "^24.1.0",
"tslint": "^5.20.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.1.0",
"typescript": "^3.6.4"
}
}
+7 -1
View File
@@ -7,7 +7,13 @@ let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
+41 -4
View File
@@ -4,7 +4,7 @@ export type Files = Map<string, string>;
export type FileTransform = (files: Files) => Promise<Files>;
export enum ElectronVersionState {
export enum VersionState {
ready = 'ready',
downloading = 'downloading',
unknown = 'unknown'
@@ -14,24 +14,39 @@ export enum ElectronVersionSource {
remote = 'remote',
local = 'local'
}
export interface NpmVersion {
export interface Version {
version: string;
}
export interface NpmVersion extends Version {
name?: string;
localPath?: string;
}
export interface NodeVersion extends Version {
state: VersionState;
}
export interface EditorValues {
main: string;
renderer: string;
html: string;
preload: string;
package?: string;
}
export interface ElectronVersion extends NpmVersion {
state: ElectronVersionState;
state: VersionState;
source: ElectronVersionSource;
}
export interface SetFiddleOptions {
filePath?: string;
templateName?: string;
gistId?: string;
}
export interface OutputEntry {
text: string;
timestamp: number;
@@ -53,6 +68,28 @@ export interface Templates {
[index: string]: string | Templates;
}
export type EditorId = 'main' | 'renderer' | 'html';
// Editors
export const enum EditorId {
'main' = 'main',
'renderer' = 'renderer',
'html' = 'html',
'preload' = 'preload'
}
// Panels that can show up as a mosaic
export const enum PanelId {
'docsDemo' = 'docsDemo'
}
export type MosaicId = EditorId | PanelId;
export const ALL_EDITORS = [ EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html ];
export const ALL_PANELS = [ PanelId.docsDemo ];
export const ALL_MOSAICS = [ ...ALL_EDITORS, ...ALL_PANELS ];
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
export const enum DocsDemoPage {
DEFAULT = 'DEFAULT',
DEMO_APP = 'DEMO_APP'
}
+17 -3
View File
@@ -1,6 +1,7 @@
export enum IpcEvents {
OPEN_SETTINGS = 'OPEN_SETTINGS',
LOAD_GIST_REQUEST = 'LOAD_GIST_REQUEST',
LOAD_ELECTRON_EXAMPLE_REQUEST = 'LOAD_ELECTRON_EXAMPLE_REQUEST',
FIDDLE_RUN = 'FIDDLE_RUN',
FIDDLE_PACKAGE = 'FIDDLE_PACKAGE',
FIDDLE_MAKE = 'FIDDLE_MAKE',
@@ -16,18 +17,26 @@ export enum IpcEvents {
FS_SAVE_FIDDLE_DIALOG = 'FS_SAVE_FIDDLE_DIALOG',
FS_SAVE_FIDDLE_ERROR = 'FS_SAVE_FIDDLE_ERROR',
SHOW_WARNING_DIALOG = 'SHOW_WARNING_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR'
SHOW_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
CLEAR_CONSOLE = 'CLEAR_CONSOLE',
LOAD_LOCAL_VERSION_FOLDER = 'LOAD_LOCAL_VERSION_FOLDER',
SHOW_LOCAL_VERSION_FOLDER_DIALOG = 'SHOW_LOCAL_VERSION_FOLDER_DIALOG',
BISECT_COMMANDS_TOGGLE = 'BISECT_COMMANDS_TOGGLE',
}
export const ipcMainEvents = [
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
IpcEvents.FS_SAVE_FIDDLE,
IpcEvents.SHOW_WARNING_DIALOG
IpcEvents.SHOW_WARNING_DIALOG,
IpcEvents.SHOW_CONFIRMATION_DIALOG,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG
];
export const ipcRendererEvents = [
IpcEvents.OPEN_SETTINGS,
IpcEvents.LOAD_GIST_REQUEST,
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
IpcEvents.FIDDLE_RUN,
IpcEvents.FIDDLE_PACKAGE,
IpcEvents.FIDDLE_MAKE,
@@ -40,5 +49,10 @@ export const ipcRendererEvents = [
IpcEvents.FS_SAVE_FIDDLE_AS,
IpcEvents.FS_SAVE_FIDDLE_FORGE,
IpcEvents.FS_SAVE_FIDDLE_GIST,
IpcEvents.SHOW_WELCOME_TOUR
IpcEvents.SHOW_WELCOME_TOUR,
IpcEvents.CLEAR_CONSOLE,
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
IpcEvents.BISECT_COMMANDS_TOGGLE
];
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
+13
View File
@@ -18,6 +18,15 @@
background-color: @background-1;
}
.bp3-button.bp3-minimal {
background-color: unset;
}
.bp3-button:hover,
.bp3-button.bp3-minimal:hover {
background-color: rgba(138, 155, 168, 0.15);
}
.bp3-menu-item.bp3-active.bp3-intent-primary {
background-color: @foreground-3;
}
@@ -29,4 +38,8 @@
background-color: @background-3;
}
}
.bp3-running-text {
font-size: 14px;
}
}
@@ -1,3 +0,0 @@
button.version-chooser.bp3-button {
border-radius: 3px 0 0 3px;
}
+8 -1
View File
@@ -2,7 +2,6 @@
header {
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
//border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background-color: @background-3;
-webkit-app-region: drag;
}
@@ -20,6 +19,7 @@ header {
select {
-webkit-app-region: no-drag;
margin-bottom: 0;
white-space: nowrap;
}
select {
@@ -27,6 +27,13 @@ header {
margin-left: 5px;
}
fieldset {
border: 0;
padding: 0;
margin: 0;
min-width: 0;
}
& > div {
display: flex;
+22
View File
@@ -2,6 +2,28 @@
animation: fadein 0.15s;
}
.mosaic-window-toolbar {
& > div {
display: flex;
justify-content: space-between;
width: 100%;
padding: 0 5px 0 5px;
.mosaic-controls {
transform: scale(0.75);
transform-origin: right;
}
h5 {
margin: 4px 0 0 0;
}
button {
margin-left: 8px;
}
}
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
+2 -2
View File
@@ -1,7 +1,7 @@
@import "../variables.less";
.fiddle {
.mosaic-window-toolbar.draggable {
.mosaic-window-toolbar {
background: var(--background-1);
box-shadow: inset 0 0 100px 100px rgba(53, 53, 53, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.1);
@@ -34,9 +34,9 @@
flex-wrap: nowrap;
align-items: stretch;
align-content: stretch;
font-size: 12px;
overflow: visible;
z-index: 4;
background-color: @background-4;
}
.mosaic {
+3 -9
View File
@@ -7,21 +7,15 @@
color: var(--text-color-1);
background: var(--background-2);
box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
border-top: @border;
top: 10vh;
padding: 0;
padding-top: 10px;
padding-left: 10px;
height: 0;
height: 100%;
overflow-y: scroll;
transition: height 0.2s;
transition: padding 0.2s;
-webkit-app-region: no-drag;
&.showing {
border-top: @border;
padding-top: 10px;
height: 150px;
}
p {
margin-bottom: 0;
white-space: pre;
+21
View File
@@ -0,0 +1,21 @@
.show-me-panel {
padding: 10px;
overflow: scroll;
& > *:first-child {
margin-top: 0;
};
.bp3-callout {
margin-bottom: 10px;
}
}
.show-me-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
list-style: none;
padding: 0;
max-height: 400px;
}
-1
View File
@@ -188,7 +188,6 @@
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
}
+1 -2
View File
@@ -3,7 +3,6 @@
// Variables, also imported in each file
@import "variables.less";
@import "main.less";
@import "container.less";
@import "mosaic-vendor.less";
@@ -17,4 +16,4 @@
@import "components/chrome-mac.less";
@import "components/editors.less";
@import "components/tour.less";
@import "components/commands-version-chooser.less";
@import "components/show-me.less";
+8 -3
View File
@@ -15,6 +15,11 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
label: 'Run Fiddle',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
id: 'clear_console',
label: 'Clear Console',
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
},
{
type: 'separator'
}
@@ -136,17 +141,17 @@ export function createContextMenu(browserWindow: BrowserWindow) {
{
id: 'cut',
label: 'Cut',
role: editFlags.canCut ? 'cut' : '',
role: 'cut',
enabled: editFlags.canCut
}, {
id: 'copy',
label: 'Copy',
role: editFlags.canCopy ? 'copy' : '',
role: 'copy',
enabled: editFlags.canCopy
}, {
id: 'paste',
label: 'Paste',
role: editFlags.canPaste ? 'paste' : '',
role: 'paste',
enabled: editFlags.canPaste
}, {
type: 'separator'
+27
View File
@@ -0,0 +1,27 @@
import { isDevMode } from '../utils/devmode';
/**
* Installs developer tools if we're in dev mode.
*
* @export
* @returns {Promise<void>}
*/
export async function setupDevTools(): Promise<void> {
if (!isDevMode()) return;
const {
default: installExtension,
REACT_DEVELOPER_TOOLS,
REACT_PERF
} = require('electron-devtools-installer');
try {
const react = await installExtension(REACT_DEVELOPER_TOOLS);
console.log(`installDevTools: Installed ${react}`);
const perf = await installExtension(REACT_PERF);
console.log(`installDevTools: Installed ${perf}`);
} catch (error) {
console.warn(`installDevTools: Error occured:`, error);
}
}
+35 -1
View File
@@ -1,4 +1,4 @@
import { dialog } from 'electron';
import { dialog, IpcMainEvent } from 'electron';
import { IpcEvents } from '../ipc-events';
import { ipcMainManager } from './ipc';
import { getOrCreateMainWindow } from './windows';
@@ -12,6 +12,15 @@ export function setupDialogs() {
ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, (_event, args) => {
showWarningDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_CONFIRMATION_DIALOG, (_event, args) => {
showConfirmationDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
await showOpenDialog(event);
});
}
/**
@@ -25,3 +34,28 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
...args
});
}
/**
* Shows a confirmation dialog
*
* @param {Electron.MessageBoxOptions} args
*/
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args
});
}
async function showOpenDialog(event: IpcMainEvent) {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Folder',
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
return;
}
event.reply(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, [filePaths[0]]);
}
+21 -21
View File
@@ -19,41 +19,41 @@ export function setupFileListeners() {
* Shows the "Open Fiddle" dialog and forwards
* the path to the renderer
*/
export function showOpenDialog() {
dialog.showOpenDialog({
export async function showOpenDialog() {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Fiddle',
properties: ['openDirectory']
}, (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
});
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
}
/**
* Shows the "Save Fiddle" dialog and forwards
* the path to the renderer
*/
export function showSaveDialog(event?: IpcEvents, as?: string) {
export async function showSaveDialog(event?: IpcEvents, as?: string) {
// We want to save to a folder, so we'll use an open dialog here
dialog.showOpenDialog({
const { filePaths } = await dialog.showOpenDialog({
buttonLabel: 'Save here',
properties: ['openDirectory', 'createDirectory'],
title: `Save Fiddle${as ? ` as ${as}` : ''}`
}, async (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
});
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
}
/**
+8 -8
View File
@@ -7,9 +7,9 @@ import { isDevMode } from '../utils/devmode';
* Is this the first run of Fiddle? If so, perform
* tasks that we only want to do in this case.
*/
export function onFirstRunMaybe() {
export async function onFirstRunMaybe() {
if (isFirstRun()) {
promptMoveToApplicationsFolder();
await promptMoveToApplicationsFolder();
}
}
@@ -17,18 +17,18 @@ export function onFirstRunMaybe() {
* Ask the user if the app should be moved to the
* applications folder.
*/
function promptMoveToApplicationsFolder(): void {
async function promptMoveToApplicationsFolder(): Promise<void> {
if (process.platform !== 'darwin') return;
if (isDevMode() || app.isInApplicationsFolder()) return;
dialog.showMessageBox({
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Move to Applications Folder', 'Do Not Move'],
defaultId: 0,
message: 'Move to Applications Folder?',
}, (response) => {
if (response === 0) {
app.moveToApplicationsFolder();
}
});
if (response === 0) {
app.moveToApplicationsFolder();
}
}
+21 -1
View File
@@ -1,6 +1,7 @@
import { ipcMain } from 'electron';
import { EventEmitter } from 'events';
import { IpcEvents, ipcMainEvents } from '../ipc-events';
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { getOrCreateMainWindow } from './windows';
/**
@@ -12,12 +13,26 @@ import { getOrCreateMainWindow } from './windows';
* @extends {EventEmitter}
*/
export class IpcMainManager extends EventEmitter {
public readyWebContents = new WeakSet<Electron.WebContents>();
private messageQueue = new WeakMap<Electron.WebContents, Array<[IpcEvents, Array<any> | undefined]>>();
constructor() {
super();
ipcMainEvents.forEach((name) => {
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);
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);
}
});
}
/**
@@ -31,6 +46,11 @@ export class IpcMainManager extends EventEmitter {
public send(channel: IpcEvents, args?: Array<any>, target?: Electron.WebContents) {
const _target = target || getOrCreateMainWindow().webContents;
const _args = args || [];
if (!this.readyWebContents.has(_target)) {
const existing = this.messageQueue.get(_target) || [];
this.messageQueue.set(_target, [...existing, [channel, args]]);
return;
}
_target.send(channel, ..._args);
}
+16 -2
View File
@@ -1,6 +1,8 @@
import { app } from 'electron';
import { isDevMode } from '../utils/devmode';
import { setupAboutPanel } from '../utils/set-about-panel';
import { setupDevTools } from './devtools';
import { setupDialogs } from './dialogs';
import { onFirstRunMaybe } from './first-run';
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
@@ -13,10 +15,11 @@ import { getOrCreateMainWindow } from './windows';
* the method that takes care of booting the application.
*/
export async function onReady() {
onFirstRunMaybe();
await onFirstRunMaybe();
if (!isDevMode()) process.env.NODE_ENV = 'production';
getOrCreateMainWindow();
setupAboutPanel();
const { setupMenu } = await import('./menu');
const { setupFileListeners } = await import('./files');
@@ -26,6 +29,16 @@ export async function onReady() {
setupFileListeners();
setupUpdates();
setupDialogs();
setupDevTools();
}
/**
* Handle the "before-quit" event
*
* @export
*/
export function onBeforeQuit() {
(global as any).isQuitting = true;
}
/**
@@ -55,13 +68,14 @@ export function main() {
}
// Set the app's name
app.setName('Electron Fiddle');
app.name = 'Electron Fiddle';
// Ensure that there's only ever one Fiddle running
listenForProtocolHandler();
// Launch
app.on('ready', onReady);
app.on('before-quit', onBeforeQuit);
app.on('window-all-closed', onWindowsAllClosed);
app.on('activate', getOrCreateMainWindow);
}
+15 -9
View File
@@ -121,6 +121,7 @@ function getTasksMenu(): MenuItemConstructorOptions {
const tasksMenu: Array<MenuItemConstructorOptions> = [
{
label: 'Run Fiddle...',
accelerator: 'F5',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
@@ -143,7 +144,7 @@ function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConst
if (typeof item === 'string') {
return {
label: key,
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [ key ])
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
};
}
@@ -159,10 +160,10 @@ function getShowMeMenu(): MenuItemConstructorOptions {
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
return {
label: 'Show Me',
submenu: showMeMenu
};
return {
label: 'Show Me',
submenu: showMeMenu
};
}
/**
@@ -205,7 +206,7 @@ function getFileMenu(): MenuItemConstructorOptions {
type: 'separator'
},
{
label: 'Save to Gist',
label: 'Publish to Gist',
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
},
{
@@ -247,14 +248,19 @@ export function setupMenu() {
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
item.submenu = item.submenu.filter((subItem) => subItem.label !== 'Toggle Developer Tools'); // Remove "Toggle Developer Tools"
item.submenu.push({ type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }); // Add zooming actions
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' ]),
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' ]),
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',
});
}
+49 -5
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import * as nodeUrl from 'url';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
const PROTOCOL = 'electron-fiddle';
@@ -16,21 +17,61 @@ const handlePotentialProtocolLaunch = (url: string) => {
}
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
if (!parsed.pathname) return;
if (!parsed.pathname || !parsed.hostname) return;
const pathParts = parsed.pathname.split('/');
if (pathParts.length <= 0 || pathParts.length > 2) return;
const pathParts = parsed.pathname.split('/').slice(1);
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [parsed.pathname]);
switch (parsed.hostname) {
// electron-fiddle://gist/blub
case 'gist':
if (pathParts.length === 1) {
// We only have a gist ID
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],
}]);
} else {
// This is a super invalid gist launch
return;
}
break;
// electron-fiddle://electron/{ref}/{path}
case 'electron':
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('/'),
}]);
} else {
// This is an invalid electron launch
return;
}
break;
default:
return;
}
};
export const scanArgv = (argv: Array<string>) => {
const protocolArg = argv.find((arg) => arg.startsWith(`${PROTOCOL}://`));
if (protocolArg) {
console.info('Found protocol arg in argv:', protocolArg);
handlePotentialProtocolLaunch(protocolArg);
}
};
export const scanNpmArgv = (argv: string) => {
const parsedArgv = JSON.parse(argv);
const { original: args } = parsedArgv;
scanArgv(args);
};
export const listenForProtocolHandler = () => {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -41,7 +82,10 @@ export const listenForProtocolHandler = () => {
}
});
if (process.platform === 'win32') {
// pass protocol URL via npm start args in dev mode
if (isDevMode() && process.env.npm_config_argv) {
scanNpmArgv(process.env.npm_config_argv);
} else if (process.platform === 'win32') {
scanArgv(process.argv);
}
};
+2 -1
View File
@@ -20,7 +20,8 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
acceptFirstMouse: true,
backgroundColor: '#1d2427',
webPreferences: {
webviewTag: false
webviewTag: false,
nodeIntegration: true
}
};
}
+91 -44
View File
@@ -1,13 +1,25 @@
import { when } from 'mobx';
import * as MonacoType from 'monaco-editor';
import { EditorValues } from '../interfaces';
import { ipcRenderer } from 'electron';
import {
ALL_EDITORS,
EditorId,
EditorValues,
SetFiddleOptions
} from '../interfaces';
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { updateEditorLayout } from '../utils/editor-layout';
import { getEditorValue } from '../utils/editor-value';
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { isEditorBackup } from '../utils/type-checks';
import { FileManager } from './file-manager';
import { RemoteLoader } from './remote-loader';
import { Runner } from './runner';
import { appState } from './state';
import { getTheme } from './themes';
import { TouchBarManager } from './touch-bar-manager';
/**
* The top-level class controlling the whole app. This is *not* a React component,
@@ -20,27 +32,29 @@ export class App {
public monaco: typeof MonacoType | null = null;
public state = appState;
public fileManager = new FileManager(appState);
public remoteLoader = new RemoteLoader(appState);
public runner = new Runner(appState);
public touchBarManager: TouchBarManager | undefined;
constructor() {
this.getValues = this.getValues.bind(this);
this.setValues = this.setValues.bind(this);
this.getEditorValues = this.getEditorValues.bind(this);
this.setEditorValues = this.setEditorValues.bind(this);
if (process.platform === 'darwin') {
this.touchBarManager = new TouchBarManager(appState);
}
}
/**
* Sets the values on all three editors.
*
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async setValues(values: Partial<EditorValues>, warn: boolean = true): Promise<boolean> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (this.state.isUnsaved && warn) {
public async replaceFiddle(
editorValues: Partial<EditorValues>,
{ 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.setWarningDialogTexts({
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
this.state.isWarningDialogShowing = true;
await when(() => !this.state.isWarningDialogShowing);
@@ -49,20 +63,15 @@ export class App {
}
}
const { main, html, renderer } = fiddle.editors;
// set values once prompt approves
await this.setEditorValues(editorValues);
if (html && html.setValue && values.html) {
html.setValue(values.html);
}
if (main && main.setValue && values.main) {
main.setValue(values.main);
}
if (renderer && renderer.setValue && values.renderer) {
renderer.setValue(values.renderer);
}
document.title = getTitle(this.state);
this.state.gistId = gistId || '';
this.state.localPath = filePath;
this.state.templateName = templateName;
// once loaded, we have a "saved" state
this.state.isUnsaved = false;
this.setupUnsavedOnChangeListener();
@@ -70,25 +79,56 @@ export class App {
}
/**
* Gets the values on all three editors.
* Sets the values on all three editors.
*
* @returns {EditorValues}
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async getValues(options?: PackageJsonOptions): Promise<EditorValues> {
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
for (const name of ALL_EDITORS) {
const editor = fiddle.editors[name];
const backup = this.state.closedPanels[name];
if (typeof values[name] !== 'undefined') {
if (isEditorBackup(backup) && backup.model) {
// The editor does not exist, attempt to set it on the backup
backup.model.setValue(values[name]!);
} else if (editor && editor.setValue) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
}
}
}
}
/**
* Gets the values on all three editors.
*
* @returns {EditorValues}
*/
public async getEditorValues(
options?: PackageJsonOptions
): Promise<EditorValues> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
const { main, html, renderer } = fiddle.editors;
const values: EditorValues = {
html: html && html.getValue() ? html.getValue() : '',
main: main && main.getValue() ? main.getValue() : '',
renderer: renderer && renderer.getValue() ? renderer.getValue() : '',
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
if (options && options.include !== false) {
values.package = await getPackageJson(this.state, values, options);
}
@@ -104,16 +144,18 @@ export class App {
const React = await import('react');
const { render } = await import('react-dom');
const { Header } = await import('./components/header');
const { Dialogs } = await import('./components/dialogs');
const { Editors } = await import('./components/editors');
const { OutputEditorsWrapper } = await import(
'./components/output-editors-wrapper'
);
const { Header } = await import('./components/header');
const className = `${process.platform} container`;
const app = (
<div className={className}>
<Header appState={this.state} />
<Dialogs appState={this.state} />
<Editors appState={this.state} />
<Header appState={this.state} />
<OutputEditorsWrapper appState={this.state} />
</div>
);
@@ -121,7 +163,9 @@ export class App {
this.setupResizeListener();
// Todo: A timer here is terrible. Let's fix this
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
// TODO: A timer here is terrible. Let's fix this
// and ensure we actually do it once Editors have mounted.
setTimeout(() => {
this.setupUnsavedOnChangeListener();
@@ -151,7 +195,9 @@ export class App {
* @returns {Promise<void>}
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
if (tag && theme.css) {
@@ -175,6 +221,7 @@ export class App {
}
window.ElectronFiddle = window.ElectronFiddle || {};
window.ElectronFiddle.contentChangeListeners = window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.contentChangeListeners =
window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
window.ElectronFiddle.app.setup();
+46 -13
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { USER_DATA_PATH } from './constants';
import { getOfflineTypeDefinitionPath } from './fetch-types';
/**
* The binary manager takes care of downloading Electron versions
@@ -19,31 +20,48 @@ export class BinaryManager {
* four times before giving up if an error occurs.
*
* @param {string} iVersion
* @param {number} iteration
* @returns {Promise<void>}
*/
public async remove(iVersion: string, i: number = 0): Promise<void> {
public async remove(iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
let isDeleted = false;
try {
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
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);
if (counter < 4) {
console.log(`Binary Manager: Trying again to run ${func.name}`);
await rerunner(func, counter + 1);
}
}
};
const binaryCleaner = async () => {
if (await this.getIsDownloaded(version)) {
// This is necessary since we're messing with .asar files inside
// the Electron binaries. Electron, powering Fiddle, will try to
// "correct" our calls, but we don't want that right here.
process.noAsar = true;
await fs.remove(this.getDownloadPath(version));
process.noAsar = false;
}
} catch (error) {
console.warn(`Binary Manager: Tried to remove ${version}, but failed`, error);
if (i < 3) {
console.log(`Binary Manager: Trying again`);
return this.remove(version, i + 1);
isDeleted = true;
}
};
const typeDefsCleaner = async () => {
await this.removeTypeDefsForVersion(version);
};
await rerunner(binaryCleaner);
if (isDeleted) {
await rerunner(typeDefsCleaner);
}
}
@@ -89,7 +107,7 @@ export class BinaryManager {
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
// Todo: Handle this case
// TODO: Handle this case
}
this.state[version] = 'ready';
@@ -160,6 +178,20 @@ export class BinaryManager {
return fs.existsSync(expectedPath);
}
public async removeTypeDefsForVersion(version: string) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const _version = normalizeVersion(version);
const typeDefsDir = path.dirname(getOfflineTypeDefinitionPath(_version));
if (fs.existsSync(typeDefsDir)) {
try {
await fs.remove(typeDefsDir);
} catch (error) {
throw error;
}
}
}
/**
* Gets the expected path for a given Electron version
*
@@ -170,8 +202,9 @@ export class BinaryManager {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actaully use it.
* Unzips an electron package so that we can actually use it.
*
* @param {string} zipPath
* @param {string} extractPath
+58
View File
@@ -0,0 +1,58 @@
import { ElectronVersion } from '../interfaces';
export class Bisector {
public revList: Array<ElectronVersion>;
public minRev: number;
public maxRev: number;
private pivot: number;
constructor(revList: Array<ElectronVersion>) {
this.getCurrentVersion = this.getCurrentVersion.bind(this);
this.continue = this.continue.bind(this);
this.calculatePivot = this.calculatePivot.bind(this);
this.revList = revList;
this.minRev = 0;
this.maxRev = revList.length - 1;
this.calculatePivot();
}
public getCurrentVersion() {
return this.revList[this.pivot];
}
public continue(isGoodVersion: boolean) {
let isBisectOver = false;
if (this.maxRev - this.minRev <= 1) {
isBisectOver = true;
}
if (isGoodVersion) {
const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
this.minRev = this.pivot;
if (upPivot !== this.maxRev && upPivot !== this.pivot) {
this.pivot = upPivot;
} else {
isBisectOver = true;
}
} else {
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;
} else {
isBisectOver = true;
}
}
if (isBisectOver) {
return [this.revList[this.minRev], this.revList[this.maxRev]];
} else {
return this.revList[this.pivot];
}
}
private calculatePivot() {
this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
}
}
+16 -1
View File
@@ -1,3 +1,4 @@
import { remote } from 'electron';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -10,11 +11,25 @@ export interface ChromeMacProps {
@observer
export class ChromeMac extends React.Component<ChromeMacProps> {
public handleDoubleClick = () => {
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
const win = remote.getCurrentWindow();
if (doubleClickAction === 'Minimize') {
win.minimize();
} else if (doubleClickAction === 'Maximize') {
if (!win.isMaximized()) {
win.maximize();
} else {
win.unmaximize();
}
}
}
public render() {
if (process.platform !== 'darwin') return null;
return (
<div className='chrome drag'>
<div className='chrome drag' onDoubleClick={this.handleDoubleClick}>
<small>{getTitle(this.props.appState)}</small>
</div>
);
@@ -4,10 +4,9 @@ import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
import { getTitle } from '../../utils/get-title';
import { IpcEvents } from '../../ipc-events';
import { idFromUrl, urlFromId } from '../../utils/gist';
import { getOctokit } from '../../utils/octokit';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface AddressBarProps {
@@ -16,21 +15,32 @@ export interface AddressBarProps {
export interface AddressBarState {
value: string;
loaders: {
gist: any;
example: any;
};
}
@observer
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
constructor(props: AddressBarProps) {
super(props);
this.loadFiddle = this.loadFiddle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.submit = this.submit.bind(this);
const { gistId } = this.props.appState;
const value = gistId ? urlFromId(gistId) : '';
this.state = { value };
const { remoteLoader } = window.ElectronFiddle.app;
this.state = {
value,
loaders: {
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
}
};
}
/**
@@ -39,15 +49,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
*
* @param {React.SyntheticEvent<HTMLFormElement>} event
*/
public async handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
public handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
event.preventDefault();
this.submit();
}
this.props.appState.gistId = idFromUrl(this.state.value) || this.state.value;
console.log('Loading');
/**
* Commit the address bar's value to app state and load the fiddle.
*
* @memberof AddressBar
*/
public submit() {
const { remoteLoader } = window.ElectronFiddle.app;
if (this.state.value) {
this.loadFiddle();
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
}
}
@@ -55,10 +70,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
* Once the component mounts, we'll subscribe to gistId changes
*/
public componentDidMount() {
const { appState } = this.props;
const { loaders } = this.state;
reaction(
() => this.props.appState.gistId,
() => appState.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);
}
public componentWillUnmount() {
const { loaders } = this.state;
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
/**
@@ -70,69 +95,36 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
this.setState({ value: event.target.value });
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof AddressBar
*/
public async loadFiddle(): Promise<boolean> {
const { appState } = this.props;
try {
const octo = await getOctokit();
const gist = await octo.gists.get({
gist_id: appState.gistId
});
this.props.appState.setWarningDialogTexts({
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
});
await window.ElectronFiddle.app.setValues({
html: gist.data.files[INDEX_HTML_NAME].content,
main: gist.data.files[MAIN_JS_NAME].content,
renderer: gist.data.files[RENDERER_JS_NAME].content,
});
document.title = getTitle(appState);
appState.localPath = null;
} catch (error) {
console.warn(`Loading Fiddle failed`, error);
return false;
}
return true;
}
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
return (
<Button
disabled={!isValueCorrect}
icon='cloud-download'
text='Load Fiddle'
onClick={this.loadFiddle}
onClick={this.submit}
/>
);
}
public render() {
const { isUnsaved } = 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 });
return (
<form className={className} onSubmit={this.handleSubmit}>
<InputGroup
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
<fieldset disabled={isPublishing}>
<InputGroup
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
</fieldset>
</form>
);
}
@@ -0,0 +1,68 @@
import { Button } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
interface BisectHandlerProps {
appState: AppState;
}
@observer
export class BisectHandler extends React.Component<BisectHandlerProps> {
constructor(props: BisectHandlerProps) {
super(props);
this.continueBisect = this.continueBisect.bind(this);
this.terminateBisect = this.terminateBisect.bind(this);
}
public continueBisect(isGood: boolean) {
const { appState } = this.props;
const response = appState.Bisector!.continue(isGood);
if (Array.isArray(response)) {
this.terminateBisect();
const [minRev, maxRev] = response;
appState.pushOutput(`[BISECT] Complete: Check between versions ${minRev.version} and ${maxRev.version}.`);
} else {
appState.setVersion(response.version);
}
}
public terminateBisect() {
const { appState } = this.props;
appState.Bisector = undefined;
}
public render() {
const { appState } = this.props;
if (!!appState.Bisector) {
return (
<>
<Button
icon={'thumbs-up'}
onClick={() => this.continueBisect(true)}
/>
<Button
icon={'thumbs-down'}
onClick={() => this.continueBisect(false)}
/>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
);
} else {
return (
<Button
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
}
}
}
@@ -0,0 +1,100 @@
import { Button, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ALL_EDITORS, MosaicId, PanelId } from '../../interfaces';
import { getVisibleMosaics } from '../../utils/editors-mosaic-arrangement';
import { AppState } from '../state';
import { TITLE_MAP } from './editors';
export interface EditorDropdownState {
value: string;
}
export interface EditorDropdownProps {
appState: AppState;
}
/**
* A dropdown allowing users to toggle the various editors
*
* @class EditorDropdown
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
*/
@observer
export class EditorDropdown extends React.Component<EditorDropdownProps, EditorDropdownState> {
constructor(props: EditorDropdownProps) {
super(props);
this.onItemClick = this.onItemClick.bind(this);
}
public render() {
return (
<>
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
<Button icon='applications' text='Editors' />
</Popover>
{this.renderDocsDemos()}
</>
);
}
public renderDocsDemos() {
if (!process.env.FIDDLE_DOCS_DEMOS) {
return null;
}
return (
<Button
icon='help'
text='Docs & Demos'
id={PanelId.docsDemo}
onClick={this.onItemClick}
active={!this.props.appState.closedPanels.docsDemo}
/>
);
}
public renderMenu() {
return (
<Menu>
{...this.renderMenuItems()}
</Menu>
);
}
public renderMenuItems() {
const { appState } = this.props;
const result: Array<JSX.Element> = [];
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
for (const id of ALL_EDITORS) {
result.push(
<MenuItem
icon={visibleMosaics.includes(id) ? 'eye-open' : 'eye-off'}
key={id}
text={TITLE_MAP[id]}
id={id}
onClick={this.onItemClick}
/>
);
}
return result;
}
public onItemClick(event: React.MouseEvent) {
const { id } = event.currentTarget;
const { appState } = this.props;
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
if (visibleMosaics.includes(id as MosaicId)) {
console.log(`EditorDropdown: Closing ${id}`);
appState.hideAndBackupMosaic(id as MosaicId);
} else {
console.log(`EditorDropdown: Opening ${id}`);
appState.showMosaic(id as MosaicId);
}
}
}
@@ -1,11 +1,11 @@
import { Button, MenuItem } from '@blueprintjs/core';
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { ElectronVersion, ElectronVersionSource, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { sortedElectronMap } from '../../utils/sorted-map';
import { AppState } from '../state';
import { getReleaseChannel } from '../versions';
@@ -24,11 +24,11 @@ export function getItemLabel({ source, state, name }: ElectronVersion): string {
if (source === ElectronVersionSource.local) {
label = name || 'Local';
} else {
if (state === ElectronVersionState.unknown) {
if (state === VersionState.unknown) {
label = `Not downloaded`;
} else if (state === ElectronVersionState.ready) {
} else if (state === VersionState.ready) {
label = `Downloaded`;
} else if (state === ElectronVersionState.downloading) {
} else if (state === VersionState.downloading) {
label = `Downloading`;
}
}
@@ -71,7 +71,7 @@ export const filterItem: ItemPredicate<ElectronVersion> = (query, { version }) =
*/
export const renderItem: ItemRenderer<ElectronVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
return null;
}
return (
@@ -95,6 +95,30 @@ export interface VersionChooserProps {
appState: AppState;
}
export const getVersionsFromAppState = (appState: AppState) => {
const { versions, versionsToShow, statesToShow } = appState;
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return false;
}
return true;
});
};
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
@@ -103,7 +127,7 @@ export interface VersionChooserProps {
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
*/
@observer
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
export class ElectronVersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
@@ -120,32 +144,29 @@ export class VersionChooser extends React.Component<VersionChooserProps, Version
this.props.appState.setVersion(version);
}
public getItems(): Array<ElectronVersion> {
const { versions, versionsToShow } = this.props.appState;
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
.filter((item) => !!item && versionsToShow.includes(getReleaseChannel(item)));
}
public render() {
const { currentElectronVersion } = this.props.appState;
const { currentElectronVersion, Bisector } = this.props.appState;
const { version } = currentElectronVersion;
return (
<ElectronVersionSelect
filterable={true}
items={this.getItems()}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
/>
</ElectronVersionSelect>
<ButtonGroup>
<ElectronVersionSelect
filterable={true}
items={getVersionsFromAppState(this.props.appState)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!Bisector}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
disabled={!!Bisector}
/>
</ElectronVersionSelect>
</ButtonGroup>
);
}
}
@@ -0,0 +1,151 @@
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, NodeVersion, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedNodeMap } from '../../utils/sorted-map';
import { AppState } from '../state';
const ElectronVersionSelect = Select.ofType<NodeVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {ElectronVersion} { source, state }
* @returns {string}
*/
export function getItemLabel({ state }: NodeVersion): string {
let label = '';
if (state === VersionState.unknown) {
label = `Not downloaded`;
} else if (state === VersionState.ready) {
label = `Downloaded`;
} else if (state === VersionState.downloading) {
label = `Downloading`;
}
return label;
}
/**
* Helper method: Returns the <Select /> icon for an Electron
* version.
*
* @param {ElectronVersion} { state }
* @returns
*/
export function getItemIcon({ state }: NodeVersion) {
return state === 'ready'
? 'saved'
: state === 'downloading' ? 'cloud-download' : 'cloud';
}
/**
* Helper method: Returns the <Select /> predicate for an Electron
* version.
*
* @param {string} query
* @param {ElectronVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<NodeVersion> = (query, { version }) => {
return version.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {ElectronVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<NodeVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={highlightText(item.version, query)}
key={item.version}
onClick={handleClick}
label={getItemLabel(item)}
icon={getItemIcon(item)}
/>
);
};
export interface VersionChooserState {
value: string;
}
export interface VersionChooserProps {
appState: AppState;
}
export const getVersionsFromAppState = (appState: AppState) => {
const { nodeVersions } = appState;
return sortedNodeMap<NodeVersion>(nodeVersions, (_key, item) => item)
.filter((item) => !!item);
};
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
*
* @class VersionChooser
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
*/
@observer
export class NodeVersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
this.onItemSelect = this.onItemSelect.bind(this);
}
/**
* Handle change, which usually means that we'd like update
* the selection version.
*
* @param {React.ChangeEvent<HTMLSelectElement>} event
*/
public onItemSelect({ version }: ElectronVersion) {
this.props.appState.setNodeVersion(version);
}
public render() {
const { currentNodeVersion, Bisector } = this.props.appState;
const { version } = currentNodeVersion;
return (
<ButtonGroup>
<ElectronVersionSelect
filterable={true}
items={getVersionsFromAppState(this.props.appState)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!Bisector}
>
<Button
className='version-chooser'
text={`Node v${version}`}
icon={getItemIcon(currentNodeVersion)}
disabled={!!Bisector}
/>
</ElectronVersionSelect>
</ButtonGroup>
);
}
}
@@ -13,10 +13,6 @@ export interface PublishButtonProps {
appState: AppState;
}
export interface PublishButtonState {
isPublishing: boolean;
}
/**
* The "publish" button takes care of logging you in.
*
@@ -25,11 +21,9 @@ export interface PublishButtonState {
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
*/
@observer
export class PublishButton extends React.Component<PublishButtonProps, PublishButtonState> {
export class PublishButton extends React.Component<PublishButtonProps> {
constructor(props: PublishButtonProps) {
super(props);
this.state = { isPublishing: false };
this.handleClick = this.handleClick.bind(this);
this.publishFiddle = this.publishFiddle.bind(this);
this.setPrivate = this.setPrivate.bind(this);
@@ -71,17 +65,13 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
* and update all related properties in the app state.
*/
public async publishFiddle(): Promise<void> {
this.setState({ isPublishing: true });
const octo = await getOctokit();
octo.authenticate({
type: 'token',
token: this.props.appState.gitHubToken!
});
const { appState } = this.props;
appState.isPublishing = true;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getValues(options);
const values = await window.ElectronFiddle.app.getEditorValues(options);
try {
const gist = await octo.gists.create({
@@ -100,7 +90,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
},
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
this.props.appState.gistId = gist.data.id;
appState.gistId = gist.data.id;
console.log(`Publish Button: Publishing done`, { gist });
} catch (error) {
@@ -114,7 +104,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
this.setState({ isPublishing: false });
appState.isPublishing = false;
}
/**
@@ -137,7 +127,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
public render() {
const { gitHubPublishAsPublic } = this.props.appState;
const { isPublishing } = this.state;
const { isPublishing } = this.props.appState;
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
@@ -158,24 +148,24 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
);
return (
<ButtonGroup className='button-publish'>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
disabled={isPublishing}
>
<Button
icon={privacyIcon}
/>
</Popover>
<Button
onClick={this.handleClick}
loading={isPublishing}
disabled={isPublishing}
icon='upload'
text={isPublishing ? 'Publishing...' : 'Publish'}
/>
</ButtonGroup>
<fieldset disabled={isPublishing}>
<ButtonGroup className='button-publish'>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
>
<Button
icon={privacyIcon}
/>
</Popover>
<Button
onClick={this.handleClick}
loading={isPublishing}
icon='upload'
text={isPublishing ? 'Publishing...' : 'Publish'}
/>
</ButtonGroup>
</fieldset>
);
}
+4 -4
View File
@@ -2,7 +2,7 @@ import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersionState } from '../../interfaces';
import { VersionState } from '../../interfaces';
import { AppState } from '../state';
export interface RunnerState {
@@ -27,11 +27,11 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
const state = currentElectronVersion && currentElectronVersion.state;
const props: IButtonProps = { className: 'button-run' };
if (state === ElectronVersionState.downloading) {
if (state === VersionState.downloading) {
props.text = 'Downloading';
props.disabled = true;
props.icon = <Spinner size={16} />;
} else if (state === ElectronVersionState.ready) {
} else if (state === VersionState.ready) {
if (isRunning) {
props.active = true;
props.text = 'Stop';
@@ -48,6 +48,6 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
props.icon = <Spinner size={16} />;
}
return <Button {...props} />;
return <Button {...props} type={undefined} />;
}
}
+32 -2
View File
@@ -4,9 +4,12 @@ import * as React from 'react';
import { AppState } from '../state';
import { AddressBar } from './commands-address-bar';
import { BisectHandler } from './commands-bisect';
import { EditorDropdown } from './commands-editors';
import { ElectronVersionChooser } from './commands-electron-chooser';
import { NodeVersionChooser } from './commands-node-chooser';
import { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
import { VersionChooser } from './commands-version-chooser';
export interface CommandsProps {
appState: AppState;
@@ -27,14 +30,24 @@ export class Commands extends React.Component<CommandsProps, {}> {
public render() {
const { appState } = this.props;
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className='commands'>
<div>
<ControlGroup fill={true} vertical={false}>
<VersionChooser appState={appState} />
{this.getVersionChooser()}
<Runner 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}
@@ -42,6 +55,13 @@ export class Commands extends React.Component<CommandsProps, {}> {
text='Console'
onClick={appState.toggleConsole}
/>
<Button
active={appState.isNodeMode}
icon='ninja'
text='Node.js Mode'
onClick={appState.toggleNodeMode}
/>
<EditorDropdown appState={appState} />
</ControlGroup>
</div>
<div>
@@ -51,4 +71,14 @@ export class Commands extends React.Component<CommandsProps, {}> {
</div>
);
}
public getVersionChooser() {
const { appState } = this.props;
if (appState.isNodeMode) {
return <NodeVersionChooser appState={appState} />;
} else {
return <ElectronVersionChooser appState={appState} />;
}
}
}
@@ -0,0 +1,160 @@
import { Button, Dialog, FileInput } from '@blueprintjs/core';
import { shell } from 'electron';
import * as fsType from 'fs-extra';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import * as React from 'react';
import { getTheme, THEMES_PATH } from '../themes';
import { AppState } from '../state';
import { defaultDark, LoadedFiddleTheme } from '../themes-defaults';
export interface AddThemeDialogProps {
appState: AppState;
}
export interface AddThemeDialogState {
file?: File;
}
/**
* The "add monaco theme" dialog allows users to add custom editor themes.
*
* @class AddThemeDialog
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
*/
@observer
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
public resetState = { file: undefined };
constructor(props: AddThemeDialogProps) {
super(props);
this.state = this.resetState;
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.reset = this.reset.bind(this);
}
/**
* Handles a change of the file input
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
const file = files && files[0] ? files[0] : undefined;
this.setState({ file });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file } = this.state;
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>;
const newTheme = defaultTheme;
const name = editor.name ? editor.name : file.name;
await this.createNewThemeFromMonaco(name, newTheme);
} catch (error) {
this.props.appState.setWarningDialogTexts({label: `Error: ${error}, please pick a different file.`});
this.props.appState.isWarningDialogShowing = true;
return;
}
this.onClose();
return;
}
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
await fsType.outputJSON(themePath, {
...newTheme,
name,
}, {spaces: 2});
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'
/>
)
];
}
public onClose() {
this.props.appState.isThemeDialogShowing = false;
this.reset();
}
public render() {
const { isThemeDialogShowing } = this.props.appState;
const inputProps = { accept: '.json' };
const { file } = this.state;
const text = file && file.path ? file.path : `Select the Monaco file...`;
return (
<Dialog
isOpen={isThemeDialogShowing}
onClose={this.onClose}
title='Add theme'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
inputProps={inputProps as any}
text={text}
/>
<br />
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
/**
* Reset this component's state
*/
private reset(): void {
this.setState(this.resetState);
return;
}
}
+31 -24
View File
@@ -6,7 +6,9 @@ import * as React from 'react';
import * as semver from 'semver';
import { NpmVersion } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { getElectronNameForPlatform } from '../../utils/electron-name';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface AddVersionDialogProps {
@@ -16,7 +18,7 @@ export interface AddVersionDialogProps {
export interface AddVersionDialogState {
isValidElectron: boolean;
isValidVersion: boolean;
file?: File;
folderPath?: string;
version: string;
}
@@ -39,8 +41,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.onChangeVersion = this.onChangeVersion.bind(this);
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
this.setFolderPath(file);
});
}
public componentWillUnmount() {
ipcRendererManager.removeAllListeners(IpcEvents.LOAD_LOCAL_VERSION_FOLDER);
}
/**
@@ -48,16 +57,11 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
public async setFolderPath(folderPath: string) {
const { binaryManager } = this.props.appState;
const file = files && files[0] ? files[0] : undefined;
const isValidElectron = !!await binaryManager.getIsDownloaded('custom', folderPath);
const isValidElectron = !!(file
&& file.path
&& await binaryManager.getIsDownloaded('custom', file.path));
this.setState({ file, isValidElectron });
this.setState({ folderPath, isValidElectron });
}
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
@@ -67,7 +71,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.setState({
version,
isValidVersion,
});
});
}
/**
@@ -76,18 +80,18 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file, version } = this.state;
const { folderPath, version } = this.state;
if (!file) return;
if (!folderPath) return;
const name = file.path
const name = folderPath
.slice(-20)
.split(path.sep)
.slice(1)
.join(path.sep);
const toAdd: NpmVersion = {
localPath: file.path,
localPath: folderPath,
version,
name
};
@@ -129,12 +133,16 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
public render() {
const { isAddVersionDialogShowing } = this.props.appState;
const inputProps = { webkitdirectory: 'true' };
const { file } = this.state;
const inputProps = {
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
}
};
const { folderPath } = this.state;
const text = file && file.path
? file.path
: `Select the folder containing ${getElectronNameForPlatform()}...`;
const text = folderPath ||
`Select the folder containing ${getElectronNameForPlatform()}...`;
return (
<Dialog
@@ -145,7 +153,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
id='custom-electron-version'
inputProps={inputProps as any}
text={text}
@@ -163,9 +170,9 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
}
private renderPath(): JSX.Element | null {
const { file, isValidElectron } = this.state;
const { folderPath, isValidElectron } = this.state;
if (!file || !file.path) return null;
if (!folderPath) return null;
const info = isValidElectron
? `We found an ${getElectronNameForPlatform()} in this folder.`
@@ -208,7 +215,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
isValidElectron: false,
isValidVersion: false,
version: '',
file: undefined
folderPath: undefined
});
}
}
+164
View File
@@ -0,0 +1,164 @@
import { Button, Dialog, Label, MenuItem } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion } from '../../interfaces';
import { Bisector } from '../bisect';
import { AppState } from '../state';
import { filterItem, getItemIcon, getVersionsFromAppState, renderItem } from './commands-electron-chooser';
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
export interface BisectDialogProps {
appState: AppState;
}
export interface BisectDialogState {
startIndex?: number;
endIndex?: number;
allVersions: Array<ElectronVersion>;
}
/**
* The "add version" dialog allows users to add custom builds of Electron.
*
* @class AddVersionDialog
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
*/
@observer
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
constructor(props: BisectDialogProps) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onBeginSelect = this.onBeginSelect.bind(this);
this.onEndSelect = this.onEndSelect.bind(this);
const allVersions = getVersionsFromAppState(this.props.appState);
this.state = { allVersions };
}
public onBeginSelect(version: ElectronVersion) {
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
}
public onEndSelect(version: ElectronVersion) {
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { endIndex, startIndex, allVersions } = this.state;
const { appState } = this.props;
if (endIndex === undefined || startIndex === undefined) {
return;
}
const bisectRange = allVersions
.slice(endIndex, startIndex + 1)
.reverse();
appState.Bisector = new Bisector(bisectRange);
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
appState.setVersion(initialBisectPivot);
this.onClose();
}
/**
* Closes the dialog
*/
public onClose() {
this.props.appState.isBisectDialogShowing = false;
}
get buttons() {
const canSubmit =
!!this.state.startIndex &&
!!this.state.endIndex &&
this.state.startIndex > this.state.endIndex;
return [
(
<Button
icon='play'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Begin'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
public render() {
const { isBisectDialogShowing } = this.props.appState;
const { startIndex, endIndex, allVersions } = this.state;
return (
<Dialog
isOpen={isBisectDialogShowing}
onClose={this.onClose}
title='Start a bisect session'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
<Label>
Earliest Version
<ElectronVersionSelect
filterable={true}
items={allVersions}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onBeginSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
text={startIndex ? `v${allVersions[startIndex].version}` : ``}
icon={startIndex ? getItemIcon(allVersions[startIndex]) : 'small-minus'}
fill={true}
/>
</ElectronVersionSelect>
</Label>
<Label>
Latest Version
<ElectronVersionSelect
filterable={true}
items={allVersions.slice(0, startIndex!)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onEndSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!startIndex}
>
<Button
text={endIndex ? `v${allVersions[endIndex].version}` : ``}
icon={endIndex ? getItemIcon(allVersions[endIndex]) : 'small-minus'}
fill={true}
disabled={!startIndex}
/>
</ElectronVersionSelect>
</Label>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
}
@@ -0,0 +1,51 @@
import { Alert, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ConfirmDialogProps {
appState: AppState;
}
export interface ConfirmDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class ConfirmDialog
* @extends {React.Component<ConfirmDialogProps, ConfirmDialogState>}
*/
@observer
export class ConfirmDialog extends React.Component<ConfirmDialogProps, ConfirmDialogState> {
constructor(props: ConfirmDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.confirmationPromptLastResult = result;
this.props.appState.toggleConfirmationPromptDialog();
}
public render() {
const { isConfirmationPromptShowing, confirmationDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isConfirmationPromptShowing}
onClose={this.onClose}
icon='help'
confirmButtonText={confirmationDialogTexts.ok}
cancelButtonText={confirmationDialogTexts.cancel}
intent={Intent.PRIMARY}
>
<p>{confirmationDialogTexts.label}</p>
</Alert>
);
}
}
+4 -9
View File
@@ -56,28 +56,23 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
public async onSubmitToken(): Promise<void> {
if (!this.state.tokenInput) return;
this.setState({ verifying: true });
this.props.appState.gitHubToken = this.state.tokenInput;
const octo = await getOctokit();
octo.authenticate({
type: 'token',
token: this.state.tokenInput,
});
const octo = await getOctokit(this.props.appState);
try {
const { data } = await octo.users.get({});
const { data } = await octo.users.getAuthenticated();
this.props.appState.gitHubAvatarUrl = data.avatar_url;
this.props.appState.gitHubLogin = data.login;
this.props.appState.gitHubName = data.name;
this.setState({ verifying: false, error: false });
} catch (error) {
console.warn(`Authenticating against GitHub failed`, error);
this.setState({ verifying: false, error: true });
return;
}
this.props.appState.gitHubToken = this.state.tokenInput;
this.props.appState.isTokenDialogShowing = false;
this.setState({ verifying: false, error: false });
}
/**
+2 -4
View File
@@ -13,8 +13,7 @@ export interface WarningDialogState {
}
/**
* The token dialog asks the user for a GitHub Personal Access Token.
* It's also responsible for checking if the token is correct.
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class WarningDialog
@@ -30,12 +29,11 @@ export class WarningDialog extends React.Component<WarningDialogProps, WarningDi
public onClose(result: boolean) {
this.props.appState.warningDialogLastResult = result;
this.props.appState.toogleWarningDialog();
this.props.appState.toggleWarningDialog();
}
public render() {
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isWarningDialogShowing}
+21 -2
View File
@@ -2,7 +2,10 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { AddThemeDialog } from './dialog-add-theme';
import { AddVersionDialog } from './dialog-add-version';
import { BisectDialog } from './dialog-bisect';
import { ConfirmDialog } from './dialog-confirm';
import { TokenDialog } from './dialog-token';
import { WarningDialog } from './dialog-warning';
import { Settings } from './settings';
@@ -21,7 +24,13 @@ export interface DialogsProps {
export class Dialogs extends React.Component<DialogsProps, {}> {
public render() {
const { appState } = this.props;
const { isTokenDialogShowing, isSettingsShowing, isAddVersionDialogShowing } = appState;
const {
isTokenDialogShowing,
isSettingsShowing,
isAddVersionDialogShowing,
isThemeDialogShowing,
isBisectDialogShowing
} = appState;
const maybeToken = isTokenDialogShowing
? <TokenDialog key='dialogs' appState={appState} />
: null;
@@ -31,13 +40,23 @@ export class Dialogs extends React.Component<DialogsProps, {}> {
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 eitherWarningOrPrompt = appState.isWarningDialogShowing
? <WarningDialog appState={appState} />
: <ConfirmDialog appState={appState} />;
return (
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
<WarningDialog appState={appState} />
{maybeMonaco}
{maybeBisect}
{eitherWarningOrPrompt}
</div>
);
}
+47 -9
View File
@@ -4,14 +4,15 @@
import * as MonacoType from 'monaco-editor';
import * as React from 'react';
import { ContentNames, getContent } from '../content';
import { EditorId } from '../../interfaces';
import { getContent } from '../content';
import { AppState } from '../state';
export interface EditorProps {
appState: AppState;
monaco: typeof MonacoType;
monoacoOptions: MonacoType.editor.IEditorOptions;
id: string;
monacoOptions: MonacoType.editor.IEditorOptions;
id: EditorId;
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
@@ -48,11 +49,16 @@ export class Editor extends React.Component<EditorProps> {
*
* @param {MonacoType.editor.IStandaloneCodeEditor} editor
*/
public editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
public async editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
const { editorDidMount } = this.props;
// Set the content on the editor
await this.setContent();
// Set the editor as an available object
window.ElectronFiddle.editors[this.props.id] = editor;
// And notify others
if (editorDidMount) {
editorDidMount(editor);
}
@@ -62,8 +68,7 @@ export class Editor extends React.Component<EditorProps> {
* Initialize Monaco.
*/
public async initMonaco() {
const { monaco, id, appState, monoacoOptions } = this.props;
const { version } = appState;
const { monaco, monacoOptions: monacoOptions } = this.props;
const ref = this.containerRef.current;
if (ref) {
@@ -71,10 +76,11 @@ export class Editor extends React.Component<EditorProps> {
language: this.language,
theme: 'main',
contextmenu: false,
value: await getContent(id as ContentNames, version),
...monoacoOptions
model: null,
...monacoOptions
});
this.editorDidMount(this.editor);
await this.editorDidMount(this.editor);
}
}
@@ -83,6 +89,7 @@ export class Editor extends React.Component<EditorProps> {
*/
public destroyMonaco() {
if (typeof this.editor !== 'undefined') {
console.log('Editor: Disposing');
this.editor.dispose();
}
}
@@ -90,4 +97,35 @@ export class Editor extends React.Component<EditorProps> {
public render() {
return <div className='editorContainer' ref={this.containerRef} />;
}
/**
* Sets the content on the editor, including the model and the view state.
*
* @private
* @memberof Editor
*/
private async setContent() {
const { appState, id, monaco } = this.props;
const { version } = appState;
const backup = appState.getAndRemoveEditorValueBackup(id);
if (backup && backup.model) {
console.log(`Editor: Backup found, restoring state`);
if (backup.viewState) {
this.editor.restoreViewState(backup.viewState);
}
this.editor.setModel(backup.model);
} else {
const value = await getContent(id, version);
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2
});
this.editor.setModel(model);
}
}
}
@@ -0,0 +1,24 @@
import * as React from 'react';
import { Button, NonIdealState } from '@blueprintjs/core';
import { EditorId } from '../../interfaces';
import { AppState } from '../state';
export function renderNonIdealState(appState: AppState) {
const allEditors = [ EditorId.html, EditorId.main, EditorId.renderer ];
const resolveButton = (
<Button
text='Open all editors'
onClick={() => appState.setVisibleMosaics(allEditors)}
/>
);
return (
<NonIdealState
action={resolveButton}
icon='applications'
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
/>
);
}
@@ -0,0 +1,88 @@
import * as React from 'react';
import { Button } from '@blueprintjs/core';
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
import { DocsDemoPage, MosaicId } from '../../interfaces';
import { AppState } from '../state';
export interface ToolbarButtonProps {
appState: AppState;
id: MosaicId;
}
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
public static contextType = MosaicWindowContext;
public context: MosaicWindowContext;
constructor(props: ToolbarButtonProps) {
super(props);
}
public render() {
return (
<MosaicContext.Consumer>
{({ mosaicActions }) => this.createButton(mosaicActions)}
</MosaicContext.Consumer>
);
}
/**
* Create a button that performs the actual action
*/
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
}
export class MaximizeButton extends ToolbarButton {
/**
* Create a button that can expand this panel
*/
public createButton(mosaicActions: MosaicRootActions<any>) {
const onClick = () => {
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
};
return (
<Button
icon='maximize'
className='bp3-small'
onClick={onClick}
/>
);
}
}
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);
return (
<Button
icon='cross'
className='bp3-small'
onClick={onClick}
/>
);
}
}
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;
return (
<Button
icon='home'
className='bp3-small'
onClick={onClick}
text='Overview'
/>
);
}
}
+153 -40
View File
@@ -1,28 +1,24 @@
import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as React from 'react';
import { Mosaic, MosaicWindow } from 'react-mosaic-component';
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
import { EditorId } from '../../interfaces';
import { EditorId, MosaicId, PanelId } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { updateEditorLayout } from '../../utils/editor-layout';
import { getFocusedEditor } from '../../utils/focused-editor';
import { getAtPath, setAtPath } from '../../utils/js-path';
import { toggleMonaco } from '../../utils/toggle-monaco';
import { ContentNames, getContent } from '../content';
import { isEditorId } from '../../utils/type-checks';
import { getContent } from '../content';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
import { activateTheme } from '../themes';
import { Editor } from './editor';
const options: any = {
direction: 'row',
first: 'main',
second: {
direction: 'column',
first: 'renderer',
second: 'html',
},
};
import { renderNonIdealState } from './editors-non-ideal-state';
import { DocsDemoGoHomeButton, MaximizeButton, RemoveButton } from './editors-toolbar-button';
import { ShowMe } from './show-me';
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
minimap: {
@@ -31,13 +27,12 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
wordWrap: 'on'
};
const ViewIdMosaic = Mosaic.ofType<EditorId>() as any;
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>() as any;
const TITLE_MAP: Record<EditorId, string> = {
main: 'Main Process',
renderer: 'Renderer Process',
html: 'HTML'
export const TITLE_MAP: Record<MosaicId, string> = {
main: 'Main Process (main.js)',
renderer: 'Renderer Process (renderer.js)',
preload: 'Preload (preload.js)',
html: 'HTML (index.html)',
docsDemo: 'Docs & Demos',
};
export interface EditorsProps {
@@ -50,13 +45,33 @@ export interface EditorsState {
monacoOptions: MonacoType.editor.IEditorOptions;
}
@observer
export class Editors extends React.Component<EditorsProps, EditorsState> {
// A reaction: Each time mosaicArrangement is changed, we'll update
// the editor layout. That method is itself debounced.
public disposeLayoutAutorun = reaction(
() => this.props.appState.mosaicArrangement,
() => updateEditorLayout()
);
constructor(props: EditorsProps) {
super(props);
this.onChange = this.onChange.bind(this);
this.renderEditor = this.renderEditor.bind(this);
this.renderTile = this.renderTile.bind(this);
this.renderGenericPanel = this.renderGenericPanel.bind(this);
this.state = { monacoOptions: defaultMonacoOptions };
this.loadMonaco();
}
/**
* Executed right after the component mounts. We'll setup the IPC listeners here.
*
* @memberof Editors
*/
public componentDidMount() {
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
this.executeCommand(cmd);
@@ -65,15 +80,11 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
const { version } = this.props.appState;
this.props.appState.setWarningDialogTexts({
label: 'Your current fiddle is unsaved. Do you want to discard it?'
});
window.ElectronFiddle.app.setValues({
html: await getContent(ContentNames.HTML, version),
renderer: await getContent(ContentNames.RENDERER, version),
main: await getContent(ContentNames.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) => {
@@ -83,6 +94,10 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
this.setState({ isMounted: true });
}
public componentWillUnmount() {
this.disposeLayoutAutorun();
}
/**
* Attempt to execute a given commandId on the currently focused editor
*
@@ -135,27 +150,122 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
}
}
/**
* Renders the little tool bar on top of each panel
*
* @param {MosaicWindowProps<MosaicId>} { title }
* @param {MosaicId} id
* @returns {JSX.Element}
*/
public renderToolbar(
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
): JSX.Element {
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
? <DocsDemoGoHomeButton id={id} appState={this.props.appState} />
: null;
return (
<div>
{/* Left */}
<div>
<h5>
{title}
</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className='mosaic-controls'>
{docsDemoGoHomeMaybe}
<MaximizeButton id={id} appState={this.props.appState} />
<RemoveButton id={id} appState={this.props.appState} />
</div>
</div>
);
}
/**
* Renders a Mosaic tile
*
* @param {string} id
* @param {string} path
* @returns {JSX.Element | null}
*/
public renderTile(id: MosaicId, path: Array<MosaicBranch>): JSX.Element {
const { appState } = this.props;
const content = isEditorId(id)
? this.renderEditor(id)
: this.renderGenericPanel(id, appState);
return (
<MosaicWindow<EditorId>
className={id}
path={path}
title={TITLE_MAP[id]}
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
>
{content}
</MosaicWindow>
);
}
/**
* Renders a generic panel  so not an editor, but something else
*
* @param {PanelId} _id
* @param {AppState} _appState
* @returns {JSX.Element}
*/
public renderGenericPanel(_id: PanelId, appState: AppState): JSX.Element {
return <ShowMe appState={appState} />;
}
/**
* Render an editor
*
* @param {EditorId} id
* @returns {(JSX.Element | null)}
* @memberof Editors
*/
public renderEditor(id: EditorId): JSX.Element | null {
const { appState } = this.props;
const { monaco } = this.state;
return (
<Editor
id={id}
monaco={monaco!}
appState={appState}
monacoOptions={defaultMonacoOptions}
/>
);
}
public render() {
const { appState } = this.props;
const { monaco } = this.state;
if (!monaco) return null;
return (
<ViewIdMosaic
onChange={updateEditorLayout}
// tslint:disable-next-line:jsx-no-multiline-js
renderTile={(id: any, path: any) => (
<ViewIdMosaicWindow className={id} path={path} title={TITLE_MAP[id]} toolbarControls={<div />}>
<Editor id={id} monaco={monaco} appState={appState} monoacoOptions={defaultMonacoOptions} />
</ViewIdMosaicWindow>
)}
initialValue={options}
<Mosaic<EditorId | PanelId>
onChange={this.onChange}
value={appState.mosaicArrangement}
zeroStateView={renderNonIdealState(appState)}
renderTile={this.renderTile}
/>
);
}
/**
* Handles a change in the visible nodes
*
* @param {(MosaicNode<EditorId> | null)} currentNode
*/
public onChange(currentNode: MosaicNode<EditorId> | null) {
this.props.appState.mosaicArrangement = currentNode;
}
/**
* Loads monaco. If it's already loaded, it'll just set it on the current state.
* We're doing things a bit roundabout to ensure that we're not overloading the
@@ -171,7 +281,10 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
}
if (!this.state || !this.state.isMounted) {
this.state = { monaco, monacoOptions: defaultMonacoOptions };
this.state = {
monaco,
monacoOptions: defaultMonacoOptions
};
} else {
this.setState({ monaco });
}
-2
View File
@@ -3,7 +3,6 @@ import * as React from 'react';
import { AppState } from '../state';
import { ChromeMac } from './chrome-mac';
import { Commands } from './commands';
import { Output } from './output';
import { WelcomeTour } from './tour-welcome';
export interface HeaderProps {
@@ -23,7 +22,6 @@ export class Header extends React.Component<HeaderProps, {}> {
<ChromeMac appState={this.props.appState} />
<header id='header'>
<Commands key='commands' appState={this.props.appState} />
<Output key='output' appState={this.props.appState} />
</header>
<WelcomeTour appState={this.props.appState} />
</>
@@ -0,0 +1,59 @@
import * as React from 'react';
import { Mosaic, MosaicNode } from 'react-mosaic-component';
import { AppState } from '../state';
import { Editors } from './editors';
import { Output } from './output';
export interface WrapperProps {
appState: AppState;
}
export interface WrapperState {
mosaicArrangement: MosaicNode<WrapperMosaicId>;
}
export type WrapperMosaicId = 'output' | 'editors';
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
private MOSAIC_ELEMENTS = {
output: <Output appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />
};
constructor(props: any) {
super(props);
this.state = {
mosaicArrangement: {
direction: 'column',
first: 'output',
second: 'editors',
splitPercentage: 25,
}
};
}
public render() {
return (
<>
<Mosaic<WrapperMosaicId>
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
resize={{minimumPaneSizePercentage: 0}}
value={this.state.mosaicArrangement}
onChange={this.onChange}
/>
</>
);
}
private onChange = (currentNode: any) => {
const isConsoleShowing = currentNode.splitPercentage !== 0;
if (isConsoleShowing !== this.props.appState.isConsoleShowing) {
this.props.appState.isConsoleShowing = isConsoleShowing;
}
this.setState({mosaicArrangement: currentNode});
}
}
+30 -4
View File
@@ -1,8 +1,12 @@
import { autorun } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MosaicContext } from 'react-mosaic-component';
import { OutputEntry } from '../../interfaces';
import { AppState } from '../state';
import { WrapperMosaicId } from './output-editors-wrapper';
export interface CommandsProps {
appState: AppState;
@@ -19,6 +23,8 @@ export interface CommandsProps {
*/
@observer
export class Output extends React.Component<CommandsProps, {}> {
public static contextType = MosaicContext;
public context: MosaicContext<WrapperMosaicId>;
private outputRef = React.createRef<HTMLDivElement>();
constructor(props: CommandsProps) {
@@ -28,6 +34,23 @@ export class Output extends React.Component<CommandsProps, {}> {
this.renderEntry = this.renderEntry.bind(this);
}
public componentDidMount() {
autorun(() => {
const { isConsoleShowing } = this.props.appState;
// this context should always exist, but mocking context in enzyme
// is not fully supported, so this condition makes the tests pass
if (this.context.mosaicActions && this.context.mosaicActions.expand) {
if (!isConsoleShowing) {
this.context.mosaicActions.expand(['first'], 0);
} else {
this.context.mosaicActions.expand(['first'], 25);
}
}
});
}
public componentDidUpdate() {
if (this.outputRef.current) {
this.outputRef.current.scrollTop = this.outputRef.current.scrollHeight;
@@ -71,9 +94,12 @@ export class Output extends React.Component<CommandsProps, {}> {
));
}
public render() {
const { isConsoleShowing, output } = this.props.appState;
const className = isConsoleShowing ? 'output showing' : 'output';
public render(): JSX.Element | null {
const { output, isConsoleShowing } = this.props.appState;
if (!isConsoleShowing) {
return null;
}
// The last 1000 lines
const lines = output
@@ -81,7 +107,7 @@ export class Output extends React.Component<CommandsProps, {}> {
.map(this.renderEntry);
return (
<div className={className} ref={this.outputRef}>
<div className='output' ref={this.outputRef}>
{lines}
</div>
);
+110 -43
View File
@@ -12,9 +12,9 @@ import {
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { ElectronVersion, ElectronVersionSource, VersionState } from '../../interfaces';
import { normalizeVersion } from '../../utils/normalize-version';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { sortedElectronMap } from '../../utils/sorted-map';
import { AppState } from '../state';
import { ElectronReleaseChannel, getReleaseChannel } from '../versions';
@@ -41,6 +41,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
this.handleDownloadAll = this.handleDownloadAll.bind(this);
this.handleDeleteAll = this.handleDeleteAll.bind(this);
this.handleChannelChange = this.handleChannelChange.bind(this);
this.handleStateChange = this.handleStateChange.bind(this);
this.handleDownloadClick = this.handleDownloadClick.bind(this);
this.handleAddVersion = this.handleAddVersion.bind(this);
@@ -54,6 +55,24 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
this.props.appState.updateElectronVersions();
}
/**
* Handles a change in which channels should be displayed.
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleStateChange(
event: React.FormEvent<HTMLInputElement>
) {
const { id, checked } = event.currentTarget;
const { appState } = this.props;
if (!checked) {
appState.statesToShow = appState.statesToShow.filter((s) => s !== id);
} else {
appState.statesToShow.push(id as VersionState);
}
}
/**
* Handles a change in which channels should be displayed.
*
@@ -120,7 +139,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
return (
<div className='settings-electron'>
<h2>Electron Settings</h2>
{this.renderVersionOptions()}
<Callout>
{this.renderVersionChannelOptions()}
{this.renderVersionStateOptions()}
</Callout>
<br />
<Callout>
{this.renderAdvancedButtons()}
@@ -173,51 +195,91 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
/**
* Renders the various options for which versions should be displayed
* in the small dropdown.
*
* @private
* @returns {JSX.Element}
*/
private renderVersionOptions(): JSX.Element {
private renderVersionStateOptions(): JSX.Element {
const { appState } = this.props;
const getIsChecked = (state: VersionState) => {
return appState.statesToShow.includes(state);
};
return (
<FormGroup
label='Include Electron versions that are:'
>
<Checkbox
checked={getIsChecked(VersionState.downloading)}
label='Downloading'
id='downloading'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(VersionState.ready)}
label='Downloaded'
id='ready'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(VersionState.unknown)}
label='Not Downloaded'
id='unknown'
onChange={this.handleStateChange}
inline={true}
/>
</FormGroup>
);
}
/**
* Renders the various options for which versions should be displayed
*
* @private
* @returns {JSX.Element}
*/
private renderVersionChannelOptions(): JSX.Element {
const { appState } = this.props;
const getIsChecked = (channel: ElectronReleaseChannel) => {
return appState.versionsToShow.includes(channel);
};
return (
<Callout>
<FormGroup
label='Include Electron versions from these release channels:'
>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.stable)}
label='Stable'
id='Stable'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.beta)}
label='Beta'
id='Beta'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.nightly)}
label='Nightly'
id='Nightly'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
label='Unsupported'
id='Unsupported'
onChange={this.handleChannelChange}
inline={true}
/>
</FormGroup>
</Callout>
<FormGroup
label='Include Electron versions from these release channels:'
>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.stable)}
label='Stable'
id='Stable'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.beta)}
label='Beta'
id='Beta'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.nightly)}
label='Nightly'
id='Nightly'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
label='Unsupported'
id='Unsupported'
onChange={this.handleChannelChange}
inline={true}
/>
</FormGroup>
);
}
@@ -254,7 +316,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
* @returns {Array<JSX.Element>}
*/
private renderTableRows(): Array<JSX.Element | null> {
const { versions, versionsToShow } = this.props.appState;
const { versions, versionsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
// Check if we want to show the version
@@ -262,6 +324,11 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
return null;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return null;
}
return (
<tr key={item.version}>
<td>{item.version}</td>
@@ -273,7 +340,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
}
/**
* Returns a human-readable state indicator for an Electorn version
* Returns a human-readable state indicator for an Electron version
*
* @param {ElectronVersion} item
* @returns {JSX.Element}
@@ -283,10 +350,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
let icon: IconName = 'box';
let humanState = 'Downloaded';
if (state === ElectronVersionState.downloading) {
if (state === VersionState.downloading) {
icon = 'cloud-download';
humanState = 'Downloading';
} else if (state === ElectronVersionState.unknown) {
} else if (state === VersionState.unknown) {
icon = 'cloud';
humanState = 'Not downloaded';
}
@@ -334,6 +401,6 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
buttonProps.onClick = () => appState.downloadVersion(key);
}
return <Button {...buttonProps} />;
return <Button {...buttonProps} type={undefined} />;
}
}
+37 -10
View File
@@ -20,6 +20,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
super(props);
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
}
/**
@@ -35,31 +36,57 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
this.props.appState.isKeepingUserDataDirs = checked;
}
/**
* Handles a change on whether or not electron should log more things
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleElectronLoggingChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isEnablingElectronLogging = checked;
}
public render() {
const { isKeepingUserDataDirs } = this.props.appState;
const { isKeepingUserDataDirs, isEnablingElectronLogging } = this.props.appState;
const deleteUserDirLabel = `
Whenever Electron runs, it creates a user data directory for cookies, the cache,
and various other things that it needs to keep around. Since fiddles are usually
just run once, we delete this directory after your fiddle exits. Enable this
setting to keep the user data directories around.
`.trim();
const electronLoggingLabel = `
There are some flags that Electron uses to log extra information both internally
and through Chromium. Enable this option to make Fiddle produce those logs.`.trim();
return (
<div>
<h2>Exection</h2>
<h2>Execution</h2>
<Callout>
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.'
onChange={this.handleDeleteDataChange}
/>
</FormGroup>
</Callout>
<FormGroup label={deleteUserDirLabel}>
<Checkbox
checked={isKeepingUserDataDirs}
label='Do not delete user data directories.'
onChange={this.handleDeleteDataChange}
/>
</FormGroup>
</Callout>
<br />
<Callout>
<FormGroup label={electronLoggingLabel}>
<Checkbox
checked={isEnablingElectronLogging}
label='Enable advanced Electron logging.'
onChange={this.handleElectronLoggingChange}
/>
</FormGroup>
</Callout>
</div>
);
}
@@ -76,6 +76,7 @@ export class AppearanceSettings extends React.Component<
this.handleChange = this.handleChange.bind(this);
this.openThemeFolder = this.openThemeFolder.bind(this);
this.handleAddTheme = this.handleAddTheme.bind(this);
this.state = {
themes: []
@@ -111,8 +112,9 @@ export class AppearanceSettings extends React.Component<
* @memberof AppearanceSettings
*/
public async createNewThemeFromCurrent(): Promise<boolean> {
const { appState } = this.props;
const fs = await fancyImport<typeof fsType>('fs-extra');
const theme = await getTheme(this.props.appState.theme);
const theme = await getTheme(appState.theme);
try {
const namor = await fancyImport<any>('namor');
@@ -128,6 +130,8 @@ export class AppearanceSettings extends React.Component<
shell.showItemInFolder(themePath);
this.setState({themes: await getAvailableThemes()});
return true;
} catch (error) {
console.warn(`Themes: Failed to create new theme from current`, error);
@@ -190,13 +194,29 @@ export class AppearanceSettings extends React.Component<
</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.
</p>
<Button
onClick={this.createNewThemeFromCurrent}
text='Create theme from current selection'
icon='duplicate'
/>
<Button
icon='document-open'
onClick={this.handleAddTheme}
text='Add a Monaco Editor theme'
/>
</Callout>
</div>
);
}
/**
* Opens the "add monaco theme" dialog
*/
public handleAddTheme(): void {
this.props.appState.toggleAddMonacoThemeDialog();
}
}
@@ -0,0 +1,60 @@
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ConsoleSettingsProps {
appState: AppState;
}
/**
* Settings content to manage console-related preferences.
*
* @class ConsoleSettings
* @extends {React.Component<ConsoleSettingsProps, {}>}
*/
@observer
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
constructor(props: ConsoleSettingsProps) {
super(props);
this.handleClearOnRunChange = this.handleClearOnRunChange.bind(this);
}
/**
* Handles a change on whether or not the console should be cleared
* before fiddle is executed.
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleClearOnRunChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isClearingConsoleOnRun = checked;
}
public render() {
const { isClearingConsoleOnRun } = this.props.appState;
const clearOnRunLabel = `
Enable this option to automatically clear the console whenever you run your
fiddle.`.trim();
return (
<div>
<h4>Console</h4>
<Callout>
<FormGroup label={clearOnRunLabel}>
<Checkbox
checked={isClearingConsoleOnRun}
label='Clear on run.'
onChange={this.handleClearOnRunChange}
/>
</FormGroup>
</Callout>
</div>
);
}
}
@@ -31,7 +31,7 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
return (
<Callout>
<p>
Your fiddles can be published as public GitHub Gists -
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'/>
@@ -4,6 +4,7 @@ import * as React from 'react';
import { AppState } from '../state';
import { AppearanceSettings } from './settings-general-appearance';
import { ConsoleSettings } from './settings-general-console';
import { GitHubSettings } from './settings-general-github';
export interface GeneralSettingsProps {
@@ -24,6 +25,8 @@ export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
<h2>General Settings</h2>
<AppearanceSettings appState={this.props.appState} />
<Divider />
<ConsoleSettings appState={this.props.appState} />
<Divider />
<GitHubSettings appState={this.props.appState} />
</div>
);
+20
View File
@@ -44,6 +44,16 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
this.state = {
section: SettingsSections.General
};
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
}
public componentDidMount() {
window.addEventListener('keyup', this.closeSettingsPanel, true);
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.closeSettingsPanel);
}
/**
@@ -138,4 +148,14 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
return 'cog';
}
/**
* Trigger closing of the settings panel upon Esc
*/
private closeSettingsPanel(event: KeyboardEvent) {
const { appState } = this.props;
if (event.code === 'Escape') {
appState.isSettingsShowing = false;
}
}
}
+30
View File
@@ -0,0 +1,30 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { DOCS_DEMO_COMPONENTS } from './show-me/index';
export interface ShowMeProps {
appState: AppState;
}
/**3
* The root component for "show me" content - the fourth helpful
* panel.
*
* @class ShowMe
* @extends {React.Component<ShowMeProps, ShowMeState>}
*/
@observer
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;
return (
<div className='show-me-panel'>
<Content appState={this.props.appState} />
</div>
);
}
}
+142
View File
@@ -0,0 +1,142 @@
import * as React from 'react';
import { Button, Callout, Icon } from '@blueprintjs/core';
import { remote } from 'electron';
import { renderMoreDocumentation } from './more-information';
import { getSubsetOnly } from './subset-only';
/**
* Show and hide button examples, only rendered on macOS
*
* @returns {JSX.Element | null}
*/
function ShowHide() {
if (process.platform !== 'darwin') return null;
const onClick = () => {
remote.app.hide();
setTimeout(() => {
remote.app.show();
remote.app.focus();
}, 1500);
};
return (
<>
<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'
onClick={onClick}
/>
</>
);
}
export function ShowMeApp(_props: any): JSX.Element {
const [ secondsLeft, setSeconds ] = React.useState(-1);
const playFocus = () => {
setSeconds(3);
setTimeout(() => setSeconds(2), 1000);
setTimeout(() => setSeconds(1), 2000);
setTimeout(() => {
remote.app.focus();
setSeconds(-1);
}, 3000);
};
const [ paths, setPaths ] = React.useState('');
const playPaths = () => {
const pathsToQuery = [
'home',
'appData',
'userData',
'temp',
'downloads',
'desktop'
];
let result = '';
pathsToQuery.forEach((item) => {
result += `${item}: ${remote.app.getPath(item as any)}\n`;
});
setPaths(result);
};
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'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'
>
<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.
</p>
<Button
id='focus'
icon='lightbulb'
text={`Focus Electron Fiddle${secondsLeft > 0 ? ` in ${secondsLeft}s` : ''}`}
onClick={playFocus}
/>
<ShowHide />
</Callout>
<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.
</p>
<Button
id='special-paths'
icon='play'
text={`Get special directory paths`}
onClick={playPaths}
/>
<pre id='special-paths-content'>
{paths}
</pre>
</Callout>
<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.
</p>
<Button
id='process-metrics'
icon='play'
text={`Get process metrics`}
onClick={playMetrics}
/>
<pre id='process-metrics-content'>
{metrics}
</pre>
</Callout>
{renderMoreDocumentation()}
</>
);
}
@@ -0,0 +1,43 @@
import { Button, Icon } from '@blueprintjs/core';
import * as React from 'react';
import { DocsDemoPage } from '../../../interfaces';
import { AppState } from '../../state';
import { DOCS_DEMO_COMPONENTS, DOCS_DEMO_NAMES } from './index';
import { renderMoreDocumentation } from './more-information';
export function ShowMeDefault(props: { appState: AppState }): JSX.Element {
const showMeMenu: Array<JSX.Element> = Object.keys(DOCS_DEMO_COMPONENTS)
.filter((key) => key !== DocsDemoPage.DEFAULT)
.map((key) => {
return (
<li key={key}>
<Button
minimal={true}
icon='play'
text={DOCS_DEMO_NAMES[key]}
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.
</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>
{renderMoreDocumentation()}
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { DocsDemoPage } from '../../../interfaces';
import { ShowMeApp } from './app';
import { ShowMeDefault } from './default';
export const DOCS_DEMO_COMPONENTS: Record<DocsDemoPage, any> = {
DEFAULT: ShowMeDefault,
DEMO_APP: ShowMeApp
};
export const DOCS_DEMO_NAMES: Record<DocsDemoPage, string> = {
DEFAULT: 'Home',
DEMO_APP: 'App Demos'
};
@@ -0,0 +1,32 @@
import * as React from 'react';
import { shell } from 'electron';
/**
* Renders a "more documentation" block, typically used at the end of a "show me" documentation piece.
*
* @export
* @param {string} [url='electronjs.org/docs']
* @returns {JSX.Element}
*/
export function renderMoreDocumentation(
url: string = 'electronjs.org/docs'
): JSX.Element {
const fullUrl = `https://${url}`;
const gitHubUrl = `https://github.com/electron/fiddle`;
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>
<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>
</>
);
}
@@ -0,0 +1,26 @@
import * as React from 'react';
import { shell } from 'electron';
import { getDocsUrlForModule } from '../../../utils/docs-urls';
/**
* Returns a common string we use as a disclaimer to explain that this set of demos
* is only meant to be a short demonstration, not displaying all of the modules
* abilities.
*
* @export
* @param {string} moduleName
* @returns {JSX.Element}
*/
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>
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>.
</p>
);
}
+4 -4
View File
@@ -115,7 +115,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
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
<code>BrowserWindow</code>.
&nbsp;<code>BrowserWindow</code>.
</p>
<p>
To get started, pretend that the main process is just like a Node.js
@@ -137,10 +137,10 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<p>
In the default fiddle, this HTML file is loaded in the
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
&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
<code>&lt;script /&gt;</code> tag and notice how we can call <code>
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
)
@@ -157,7 +157,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
</p>
<p>
By the way: If you want to use an <code>npm</code> module here, just
<code>require</code> it. Electron Fiddle will automatically detect that you
&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>
</>
+18
View File
@@ -1,5 +1,23 @@
import { remote } from 'electron';
import * as path from 'path';
import { MosaicNode } from 'react-mosaic-component';
import { EditorId, MosaicId } from '../interfaces';
// 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 DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'row',
first: EditorId.main,
second: {
direction: 'column',
first: EditorId.renderer,
second: EditorId.html,
}
};
export const ELECTRON_ORG = 'electron';
export const ELECTRON_REPO = 'electron';
+14 -18
View File
@@ -1,32 +1,28 @@
export const enum ContentNames {
HTML = 'html',
RENDERER = 'renderer',
MAIN = 'main'
}
import { EditorId } from '../interfaces';
/**
* Returns expected content for a given name.
*
* @export
* @param {ContentNames} name
* @param {EditorId} name
* @param {string} [version]
* @returns {Promise<string>}
*/
export async function getContent(
name: ContentNames,
version?: string
name: EditorId,
version?: string,
): Promise<string> {
if (name === ContentNames.HTML) {
if (name === EditorId.html) {
return (await import('../content/html')).html;
}
if (name === ContentNames.RENDERER) {
if (name === EditorId.renderer) {
return (await import('../content/renderer')).renderer;
}
if (name === ContentNames.MAIN) {
if (name === EditorId.main) {
// We currently only distinguish between loadFile
// and loadURL. Todo: Properly version the quick-start.
// and loadURL. TODO: Properly version the quick-start.
if (version && version.startsWith('1.')) {
return (await import('../content/main-1-x-x')).main;
}
@@ -40,18 +36,18 @@ export async function getContent(
/**
* Did the content change?
*
* @param {ContentNames} name
* @param {EditorId} name
* @returns {Promise<boolean>}
*/
export async function isContentUnchanged(name: ContentNames): Promise<boolean> {
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
const values = await window.ElectronFiddle.app.getValues({ include: false });
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
// Handle main case, which needs to check both possible versions
if (name === ContentNames.MAIN) {
const isChanged1x = await getContent(ContentNames.MAIN, '1.0') === values.main;
const isChangedOther = await getContent(ContentNames.MAIN) === values.main;
if (name === EditorId.main) {
const isChanged1x = await getContent(EditorId.main, '1.0') === values.main;
const isChangedOther = await getContent(EditorId.main) === values.main;
return isChanged1x || isChangedOther;
} else {
+58 -21
View File
@@ -2,28 +2,38 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { ElectronVersion, ElectronVersionSource } from '../interfaces';
import { callIn } from '../utils/call-in';
import { fancyImport } from '../utils/import';
import { USER_DATA_PATH } from './constants';
const definitionPath = path.join(USER_DATA_PATH, 'electron-typedef');
/**
* Fetch TypeScript definitions for the current version of Electron (online)
*
* @param {string} version
* @returns {Promise<string>}
*/
export function fetchTypeDefinitions(version: string): Promise<string> {
export async function fetchTypeDefinitions(version: string): Promise<string> {
const url = `https://unpkg.com/electron@${version}/electron.d.ts`;
return window.fetch(url)
.then((response) => response.text())
.catch((error) => {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
});
let text: string;
try {
const response = await window.fetch(url);
text = await response.text();
} catch (error) {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
}
// for invalid packa
if (text.includes('Cannot find package')) {
console.warn(`Fetch Types: ${text}`);
return '';
} else {
return text;
}
}
/**
@@ -48,27 +58,25 @@ export async function getOfflineTypeDefinitions(version: string): Promise<boolea
}
/**
* Get TypeScript defintions for a version of Electron. If none can't be
* Get TypeScript definitions for a version of Electron. If none can be
* found, returns null.
*
* @param {string} version
* @returns {void}
*/
export async function getTypeDefinitions(version: string): Promise<string | null> {
export async function getDownloadedVersionTypeDefs(version: ElectronVersion): Promise<string | null> {
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(definitionPath);
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
const offlinePath = getOfflineTypeDefinitionPath(version);
if (await getOfflineTypeDefinitions(version)) {
if (await getOfflineTypeDefinitions(version.version)) {
try {
return await fs.readFile(offlinePath, 'utf-8');
} catch (error) {
return null;
}
} else {
const typeDefs = await fetchTypeDefinitions(version);
const typeDefs = await fetchTypeDefinitions(version.version);
if (typeDefs && typeDefs.length > 0) {
try {
await fs.outputFile(offlinePath, typeDefs);
@@ -79,17 +87,27 @@ export async function getTypeDefinitions(version: string): Promise<string | null
return typeDefs;
}
}
return null;
}
}
export async function getLocalVersionTypeDefs(version: ElectronVersion) {
if (version.source === ElectronVersionSource.local && !!version.localPath) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typesPath = getLocalTypePathForVersion(version);
if (!!typesPath && fs.existsSync(typesPath)) {
return fs.readFile(typesPath, 'utf-8');
}
}
return null;
}
/**
* Tries to update the editor with type definitions.
*
* @param {string} version
*/
export async function updateEditorTypeDefinitions(version: string, i: number = 0): Promise<void> {
export async function updateEditorTypeDefinitions(version: ElectronVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -106,17 +124,36 @@ export async function updateEditorTypeDefinitions(version: string, i: number = 0
const { app } = window.ElectronFiddle;
const monaco: typeof MonacoType = app.monaco!;
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
const typeDefs = await getTypeDefinitions(version);
const getTypeDefs = (version.source === ElectronVersionSource.local) ?
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
const typeDefs = await getTypeDefs(version);
if (typeDefDisposable) {
typeDefDisposable.dispose();
}
if (typeDefs) {
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version}`);
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} 😢`);
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
}
}
export function getLocalTypePathForVersion(version: ElectronVersion) {
if (version.localPath) {
return path.join(
version.localPath,
'gen',
'electron',
'tsc',
'typings',
'electron.d.ts'
);
} else {
return null;
}
}
+37 -29
View File
@@ -3,9 +3,8 @@ 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, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { ipcRendererManager } from './ipc';
import { AppState } from './state';
@@ -38,12 +37,12 @@ export class FileManager {
/**
* Opens a template.
*
* @param {string} name
* @param {string} templateName
* @memberof FileManager
*/
public async openTemplate(name: string) {
const values = await getTemplateValues(name);
this.setFiddle(values);
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
@@ -59,33 +58,15 @@ export class FileManager {
// We'll do our best and will likely have to
// rewrite this once we support multiple files
const values: EditorValues = {
const editorValues: EditorValues = {
html: await this.readFile(path.join(filePath, INDEX_HTML_NAME)),
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)),
};
this.setFiddle(values);
}
/**
* Got values? This method will load them.
*
* @param {EditorValues} values
* @memberof FileManager
*/
public async setFiddle(values: EditorValues, filePath?: string) {
this.appState.gistId = '';
this.appState.isMyGist = false;
this.appState.localPath = filePath || null;
this.appState.setWarningDialogTexts({
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
await window.ElectronFiddle.app.setValues(values);
document.title = getTitle(this.appState);
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
}
/**
@@ -107,7 +88,15 @@ export class FileManager {
const files = await this.getFiles(undefined, ...transforms);
for (const [fileName, content] of files) {
await this.saveFile(path.join(pathToSave, fileName), content);
const savePath = path.join(pathToSave, fileName);
// If the file has content, save it to disk. If there's no
// content in the file, remove a file that possibly exists.
if (content) {
await this.saveFile(savePath, content);
} else {
await this.removeFile(savePath);
}
}
if (pathToSave !== localPath) {
@@ -130,12 +119,13 @@ export class FileManager {
*/
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
const values = await window.ElectronFiddle.app.getValues(pOptions);
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
let output: Files = new Map();
output.set(RENDERER_JS_NAME, values.renderer);
output.set(MAIN_JS_NAME, values.main);
output.set(INDEX_HTML_NAME, values.html);
output.set(PRELOAD_JS_NAME, values.preload);
output.set(PACKAGE_NAME, values.package!);
for (const transform of transforms) {
@@ -238,4 +228,22 @@ export class FileManager {
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
/**
* Safely attempts to remove a file, doesn't crash the app if
* it fails.
*
* @param {string} filePath
* @returns {string}
* @memberof FileManager
*/
private async removeFile(filePath: string): Promise<void> {
try {
const fs = await fancyImport<typeof fsType>('fs-extra');
return await fs.remove(filePath);
} catch (error) {
console.log(`FileManager: Could not remove ${filePath}`, error);
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
}
+6 -3
View File
@@ -16,7 +16,7 @@ const ignoredModules: Array<string> = [
];
/* regular expression to both match and extract module names */
const requiregx = /require\(['"](.*?)['"]\)/gm;
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
@@ -91,8 +91,11 @@ export function findModules(input: string): Array<string> {
/* grab all global require matches in the text */
// tslint:disable-next-line:no-conditional-assignment
while (match = (requiregx.exec(input) || null)) {
const mod = match[1];
modules.push(mod);
// ensure commented-out requires aren't downloaded
if (!match[0].startsWith('//')) {
const mod = match[1];
modules.push(mod);
}
}
/* map and reduce */
+259
View File
@@ -0,0 +1,259 @@
import * as Octokit from '@octokit/rest';
import { when } from 'mobx';
import { EditorId, EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-map';
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
import { getContent } from './content';
import { AppState } from './state';
import { ElectronReleaseChannel, getReleaseChannel } from './versions';
export class RemoteLoader {
constructor(private readonly appState: AppState) {
this.loadFiddleFromElectronExample.bind(this);
this.loadFiddleFromGist.bind(this);
this.verifyRemoteLoad.bind(this);
this.verifyReleaseChannelEnabled.bind(this);
this.fetchExampleAndLoad.bind(this);
this.fetchGistAndLoad.bind(this);
this.setElectronVersionWithRef.bind(this);
this.getPackageVersionFromRef.bind(this);
this.handleLoadingSuccess.bind(this);
this.handleLoadingFailed.bind(this);
}
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const ok = await this.verifyRemoteLoad('example from the Electron docs', exampleInfo.ref);
if (!ok) return;
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const ok = await this.verifyRemoteLoad('gist');
if (!ok) return;
this.fetchGistAndLoad(gistInfo.id);
}
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
const folder = await octo.repos.getContents({
owner: ELECTRON_REPO,
repo: ELECTRON_ORG,
ref,
path,
});
const ok = await this.setElectronVersionWithRef(ref);
if (!ok) return false;
const values = {
html: await getContent(EditorId.html, this.appState.version),
renderer: await getContent(EditorId.renderer, this.appState.version),
main: await getContent(EditorId.main, this.appState.version),
preload: await getContent(EditorId.preload, 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');
}
for (const child of folder.data) {
if (!child.download_url) {
console.warn(`Could not find download_url for ${child.name}`);
continue;
}
switch (child.name) {
case MAIN_JS_NAME:
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; })
);
break;
case RENDERER_JS_NAME:
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; })
);
break;
default:
break;
}
}
await Promise.all(loaders);
return this.handleLoadingSuccess(values, '');
} catch (error) {
return this.handleLoadingFailed(error);
}
}
/**
* Get data from a gist. If it doesn't exist, return an empty string.
*
* @param {Octokit.Response<Octokit.GistsGetResponse>} gist
* @param {string} name
* @returns {string}
* @memberof RemoteLoader
*/
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
try {
return gist.data.files[name].content;
} catch (error) {
return '';
}
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof RemoteLoader
*/
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
try {
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)
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
}
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
const version = await this.getPackageVersionFromRef(ref);
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
if (!supportedVersions.includes(version)) {
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);
if (!this.appState.versionsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
if (!ok) return false;
this.appState.versionsToShow.push(versionReleaseChannel);
}
this.appState.setVersion(version);
return true;
}
public async getPackageVersionFromRef(ref: string): Promise<string> {
const octo = await getOctokit(this.appState);
const { data: packageJsonData } = await octo.repos.getContents({
owner: ELECTRON_ORG,
repo: ELECTRON_REPO,
ref,
path: 'package.json'
});
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
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
});
return '0.0.0';
}
}
/**
* Verifies from the user that we should be loading this fiddle
*
* @param what What are we loading from (gist, example, etc.)
*/
public async verifyRemoteLoad(what: string, fiddlePath?: string): Promise<boolean> {
this.appState.setConfirmationPromptTexts({
label: `Are you sure you sure you want to load this '${what}' from fiddle path '${fiddlePath}'? Only load and run it if you trust the source.`
});
this.appState.isConfirmationPromptShowing = true;
await when(() => !this.appState.isConfirmationPromptShowing);
return !!this.appState.confirmationPromptLastResult;
}
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
this.appState.setWarningDialogTexts({
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?`
});
this.appState.isWarningDialogShowing = true;
await when(() => !this.appState.isWarningDialogShowing);
return !!this.appState.warningDialogLastResult;
}
/**
* Loading a fiddle from GitHub succeeded, let's move on.
*
* @param {Partial<EditorValues>} values
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, {gistId});
return true;
}
/**
* Loading a fiddle from GitHub failed - this method handles this case
* gracefully.
*
* @param {Error} error
* @returns {boolean}
*/
private handleLoadingFailed(error: Error): false {
if (navigator.onLine) {
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
} else {
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined
});
}
this.appState.toggleWarningDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
+20 -5
View File
@@ -37,14 +37,17 @@ export class Runner {
* @returns {Promise<boolean>}
*/
public async run(): Promise<boolean> {
const { fileManager, getValues } = window.ElectronFiddle.app;
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { binaryManager, currentElectronVersion } = this.appState;
const { version, localPath } = currentElectronVersion;
if (this.appState.isClearingConsoleOnRun) {
this.appState.clearConsole();
}
this.appState.isConsoleShowing = true;
const values = await getValues(options);
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
if (!dir) return false;
@@ -181,12 +184,24 @@ export class Runner {
const binaryPath = binaryManager.getElectronBinaryPath(version, localPath);
console.log(`Runner: Binary ${binaryPath} ready, launching`);
this.child = spawn(binaryPath, [ dir, '--inspect' ]);
const env = { ...process.env };
if (this.appState.isEnablingElectronLogging) {
env.ELECTRON_ENABLE_LOGGING = 'true';
env.ELECTRON_ENABLE_STACK_DUMPING = 'true';
} else {
delete env.ELECTRON_ENABLE_LOGGING;
delete env.ELECTRON_ENABLE_STACK_DUMPING;
}
this.child = spawn(binaryPath, [ dir, '--inspect' ], {
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()}.`
+272 -47
View File
@@ -1,34 +1,50 @@
import * as fsType from 'fs-extra';
import { action, autorun, computed, observable, when } from 'mobx';
import { MosaicNode } from 'react-mosaic-component';
import {
ALL_MOSAICS,
DocsDemoPage,
EditorId,
ElectronVersion,
ElectronVersionSource,
ElectronVersionState,
MosaicId,
NodeVersion,
NpmVersion,
OutputEntry,
OutputOptions,
VersionState,
WarningDialogTexts
} 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 { 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 { BinaryManager } from './binary';
import { ContentNames, getContent, isContentUnchanged } from './content';
import { updateEditorTypeDefinitions } from './fetch-types';
import { Bisector } from './bisect';
import { DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import {
addLocalVersion,
ElectronReleaseChannel,
getAllElectronVersions,
getDefaultVersion,
getElectronVersions,
getUpdatedElectronVersions,
getUpdatedNodeVersions,
saveLocalVersions
} from './versions';
const knownVersions = getElectronVersions();
const knownVersions = getAllElectronVersions();
const defaultVersion = getDefaultVersion(knownVersions);
const defaultNodeVersion = '12.0.0';
/**
* Editors exist outside of React's world. To make things *a lot*
@@ -53,6 +69,7 @@ window.ElectronFiddle = window.ElectronFiddle || {
export class AppState {
// -- Persisted settings ------------------
@observable public version: string = defaultVersion;
@observable public nodeVersion: string = defaultNodeVersion;
@observable public theme: string | null = localStorage.getItem('theme');
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
@@ -61,31 +78,54 @@ export class AppState {
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
@observable public versionsToShow: Array<ElectronReleaseChannel> =
this.retrieve('versionsToShow') as Array<ElectronReleaseChannel>
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
|| [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 binaryManager: BinaryManager = new BinaryManager();
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
// -- Various session-only state ------------------
@observable public gistId: string = '';
@observable public isMyGist: boolean = false;
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
@observable public nodeVersions: Record<string, NodeVersion> = arrayToStringMap([] as Array<NodeVersion>);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | null = null;
@observable public isUpdatingElectronVersions = false;
@observable public localPath: string | undefined;
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public confirmationDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public warningDialogLastResult: boolean | null = null;
@observable public isRunning = false;
@observable public confirmationPromptLastResult: boolean | null = null;
@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 binaryManager: BinaryManager = new BinaryManager();
@observable public Bisector: Bisector | undefined;
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isUpdatingElectronVersions: boolean = false;
// -- Various "isShowing" settings ------------------
@observable public isBisectCommandShowing: boolean;
@observable public isConsoleShowing: boolean = false;
@observable public isTokenDialogShowing: boolean = false;
@observable public isWarningDialogShowing: boolean = false;
@observable public isConfirmationPromptShowing: boolean = false;
@observable public isSettingsShowing: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isBisectDialogShowing: boolean = false;
@observable public isAddVersionDialogShowing: boolean = false;
@observable public isThemeDialogShowing: boolean = false;
@observable public isNodeMode: 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>> = {
docsDemo: true // Closed by default
};
private outputBuffer: string = '';
private name: string;
@@ -96,26 +136,36 @@ export class AppState {
this.pushOutput = this.pushOutput.bind(this);
this.removeVersion = this.removeVersion.bind(this);
this.setVersion = this.setVersion.bind(this);
this.setNodeVersion = this.setNodeVersion.bind(this);
this.showTour = this.showTour.bind(this);
this.signOutGitHub = this.signOutGitHub.bind(this);
this.toggleBisectCommands = this.toggleBisectCommands.bind(this);
this.toggleAuthDialog = this.toggleAuthDialog.bind(this);
this.toggleConsole = this.toggleConsole.bind(this);
this.toggleNodeMode = this.toggleNodeMode.bind(this);
this.clearConsole = this.clearConsole.bind(this);
this.toggleSettings = this.toggleSettings.bind(this);
this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
this.updateElectronVersions = this.updateElectronVersions.bind(this);
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);
// Setup auto-runs
autorun(() => this.save('theme', this.theme));
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('version', this.version));
autorun(() => this.save('versionsToShow', this.versionsToShow));
autorun(() => this.save('statesToShow', this.statesToShow));
autorun(() => {
if (this.isUnsaved) {
@@ -132,7 +182,16 @@ export class AppState {
// The user confirmed, let's close for real.
if (this.warningDialogLastResult) {
window.onbeforeunload = null;
window.close();
// Should we just close or quit?
const remote = require('electron').remote;
const isQuitting = remote.getGlobal('isQuitting');
if (isQuitting) {
remote.app.quit();
} else {
window.close();
}
}
});
@@ -145,6 +204,7 @@ export class AppState {
// Update our known versions
this.updateElectronVersions();
this.updateNodeVersions();
// Make sure the console isn't all empty and sad
this.pushOutput('Console ready 🔬');
@@ -162,6 +222,18 @@ export class AppState {
}
}
/**
* Returns the current NodeVersion or the first
* one that can be found.
*/
@computed get currentNodeVersion(): NodeVersion {
if (this.nodeVersions[this.nodeVersion]) {
return this.nodeVersions[this.nodeVersion];
} else {
return this.nodeVersions[defaultNodeVersion];
}
}
/**
* Update the Electron versions: First, fetch them from GitHub,
* then update their respective downloaded state.
@@ -182,6 +254,21 @@ export class AppState {
this.isUpdatingElectronVersions = false;
}
/**
* Update the Node versions: First, fetch them from Node.js,
* then update their respective downloaded state.
*
* Fails silently.
*/
@action public async updateNodeVersions() {
try {
const nodeVersions = await getUpdatedNodeVersions();
this.nodeVersions = arrayToStringMap(nodeVersions);
} catch (error) {
console.warn(`State: Could not update Node.js versions`, error);
}
}
@action public async getName() {
if (!this.name) {
this.name = await getName(this);
@@ -194,15 +281,34 @@ export class AppState {
this.isConsoleShowing = !this.isConsoleShowing;
}
@action public toggleNodeMode() {
this.isNodeMode = !this.isNodeMode;
}
@action public clearConsole() {
this.output = [];
}
@action public toggleBisectCommands() {
// guard against hiding the commands when executing a bisect
if (!this.Bisector && !this.isBisectDialogShowing) {
this.isBisectCommandShowing = !this.isBisectCommandShowing;
}
}
@action public toggleAddVersionDialog() {
this.isAddVersionDialogShowing = !this.isAddVersionDialogShowing;
}
@action public toggleAddMonacoThemeDialog() {
this.isThemeDialogShowing = !this.isThemeDialogShowing;
}
@action public toggleAuthDialog() {
this.isTokenDialogShowing = !this.isTokenDialogShowing;
}
@action public toogleWarningDialog() {
@action public toggleWarningDialog() {
this.isWarningDialogShowing = !this.isWarningDialogShowing;
if (this.isWarningDialogShowing) {
@@ -210,6 +316,18 @@ export class AppState {
}
}
@action public toggleBisectDialog() {
this.isBisectDialogShowing = !this.isBisectDialogShowing;
}
@action public toggleConfirmationPromptDialog() {
this.isConfirmationPromptShowing = !this.isConfirmationPromptShowing;
if (this.isConfirmationPromptShowing) {
this.confirmationPromptLastResult = null;
}
}
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
@@ -243,19 +361,27 @@ export class AppState {
};
}
@action public setConfirmationPromptTexts(input: WarningDialogTexts) {
this.confirmationDialogTexts = {
ok: 'Okay',
cancel: 'Cancel',
...input
};
}
@action public addLocalVersion(input: NpmVersion) {
addLocalVersion(input);
this.versions = arrayToStringMap(getElectronVersions());
this.versions = arrayToStringMap(getAllElectronVersions());
this.updateDownloadedVersionState();
}
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async removeVersion(input: string) {
const version = normalizeVersion(input);
const release = this.versions[version];
@@ -282,19 +408,19 @@ export class AppState {
saveLocalVersions(versionsAsArray);
} else {
await this.binaryManager.remove(version);
updatedVersions[version].state = ElectronVersionState.unknown;
updatedVersions[version].state = VersionState.unknown;
}
this.versions = updatedVersions;
this.updateDownloadedVersionState();
}
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async downloadVersion(input: string) {
const version = normalizeVersion(input);
console.log(`State: Downloading Electron ${version}`);
@@ -308,7 +434,7 @@ export class AppState {
console.log(`State: Instructing BinaryManager to fetch v${version}`);
const updatedVersions = { ...this.versions };
updatedVersions[version] = updatedVersions[version] || {};
updatedVersions[version].state = ElectronVersionState.downloading;
updatedVersions[version].state = VersionState.downloading;
this.versions = updatedVersions;
await this.binaryManager.setup(version);
@@ -318,12 +444,33 @@ export class AppState {
}
}
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Select a version of Node.js (and download fi necessary)
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async setNodeVersion(input: string) {
const version = normalizeVersion(input);
if (!this.nodeVersions[version]) {
console.warn(`State: Called setNodeVersion() with ${version}, which does not exist.`);
this.setVersion(this.nodeVersions[0].version);
return;
}
console.log(`State: Switching to Node ${version}`);
this.nodeVersion = version;
}
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async setVersion(input: string) {
const version = normalizeVersion(input);
@@ -339,30 +486,47 @@ export class AppState {
this.version = version;
// Should we update the editor?
if (await isContentUnchanged(ContentNames.MAIN)) {
const main = await getContent(ContentNames.MAIN, version);
window.ElectronFiddle.app.setValues({ main }, false);
if (await isContentUnchanged(EditorId.main)) {
const main = await getContent(EditorId.main, version);
window.ElectronFiddle.app.setEditorValues({ main });
}
// Update TypeScript definitions
updateEditorTypeDefinitions(version);
const versionObject = this.versions[version];
if (versionObject.source === ElectronVersionSource.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}`);
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
await updateEditorTypeDefinitions(versionObject);
});
} else {
if (!!this.localTypeWatcher) {
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
this.localTypeWatcher.close();
this.localTypeWatcher = undefined;
}
}
await updateEditorTypeDefinitions(versionObject);
// Fetch new binaries, maybe?
await this.downloadVersion(version);
}
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
@action public async updateDownloadedVersionState(): Promise<void> {
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
const updatedVersions = { ...this.versions };
(downloadedVersions || []).forEach((version) => {
if (updatedVersions[version]) {
updatedVersions[version].state = ElectronVersionState.ready;
updatedVersions[version].state = VersionState.ready;
}
});
console.log(`State: Updated version state`, updatedVersions);
@@ -394,7 +558,7 @@ export class AppState {
let strData = data.toString();
const { isNotPre, bypassBuffer } = options;
// Todo: This drops the first part of the buffer... is that fully expected?
// TODO: This drops the first part of the buffer... is that fully expected?
if (process.platform === 'win32' && bypassBuffer === false) {
this.outputBuffer += strData;
strData = this.outputBuffer;
@@ -435,6 +599,66 @@ export class AppState {
console.warn(error);
}
/**
* Sets the editor value for a given editor. Deletes the value after
* accessing it.
*
* @param {EditorId} id
*/
@action public getAndRemoveEditorValueBackup(id: EditorId): EditorBackup | null {
const value = this.closedPanels[id];
if (isEditorBackup(value)) {
delete this.closedPanels[id];
return value;
}
return null;
}
@action public setVisibleMosaics(visible: Array<MosaicId>) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
for (const id of ALL_MOSAICS) {
if (!visible.includes(id) && currentlyVisible.includes(id)) {
this.closedPanels[id] = isEditorId(id)
? getEditorBackup(id)
: true;
}
// 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)) {
delete this.closedPanels[id];
}
}
const updatedArrangement = createMosaicArrangement(visible);
console.log(`State: Setting visible mosaic panels`, visible, updatedArrangement);
this.mosaicArrangement = updatedArrangement;
}
/**
* Hides the panel for a given MosaicId.
*
* @param {MosaicId} id
*/
@action public hideAndBackupMosaic(id: MosaicId) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
this.setVisibleMosaics(currentlyVisible.filter((v) => v !== id));
}
/**
* Shows the editor value for a given editor.
*
* @param {MosaicId} id
*/
@action public showMosaic(id: MosaicId) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
this.setVisibleMosaics([...currentlyVisible, id]);
}
/**
* Resets the view, optionally with certain view flags enabled.
*
@@ -447,6 +671,7 @@ export class AppState {
this.isTourShowing = false;
this.isConsoleShowing = false;
this.isAddVersionDialogShowing = false;
this.isThemeDialogShowing = false;
if (additionalOptions) {
for (const key in additionalOptions) {
+15 -7
View File
@@ -2,12 +2,11 @@ import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { fancyImport } from '../utils/import';
/**
* Returns expected content for a given name. Currently synchronous,
* but probably shouldn't be.
* Returns expected content for a given name.
*
* @param {string} name
* @returns {Promise<EditorValues>}
@@ -15,8 +14,8 @@ import { fancyImport } from '../utils/import';
export async function getTemplateValues(name: string): Promise<EditorValues> {
const path = await fancyImport<typeof pathType>('path');
const fs = await fancyImport<typeof fsExtraType>('fs-extra');
const templatePath = path.join(__dirname, '../../static/templates', name.toLowerCase());
const templatesPath = path.join(__dirname, '../../static/show-me');
const templatePath = path.join(templatesPath, name.toLowerCase());
const getFile = async (fileName: string) => {
try {
@@ -25,7 +24,15 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
return content;
} catch (error) {
console.warn(`Could not get template file:`, error);
console.warn(`getTemplateValues(): Could not get template file:`, error);
if (fs.existsSync(templatesPath)) {
const contents = fs.readdirSync(templatesPath);
console.log(`getTemplateValues(): ${templatesPath} contents:`, contents);
} else {
console.log(`getTemplateValues(): ${templatesPath} does not exist`);
}
return '';
}
};
@@ -33,6 +40,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
return {
renderer: await getFile(RENDERER_JS_NAME),
main: await getFile(MAIN_JS_NAME),
html: await getFile(INDEX_HTML_NAME)
html: await getFile(INDEX_HTML_NAME),
preload: await getFile(PRELOAD_JS_NAME)
};
}
+283
View File
@@ -0,0 +1,283 @@
import {
remote,
TouchBarButtonConstructorOptions,
TouchBarScrubberConstructorOptions
} from 'electron';
import { autorun } from 'mobx';
import { ElectronVersion, VersionState } from '../interfaces';
import { getNiceGreeting } from '../utils/nice-greeting';
import { sortedElectronMap } from '../utils/sorted-map';
import { AppState } from './state';
const { TouchBar } = remote;
const {
TouchBarButton,
TouchBarScrubber,
TouchBarSpacer,
TouchBarLabel
} = TouchBar;
/**
* Helper method: Returns an icon (emoji, lol) for an Electron
* version.
*
* @param {Partial<ElectronVersion>} { state }
* @returns {string}
*/
export function getItemIcon(
{ state }: Partial<ElectronVersion> = { state: VersionState.unknown }
) {
return state === 'ready'
? '💾'
: state === 'downloading' ? '⏬' : '☁';
}
export class TouchBarManager {
public browserWindow: Electron.BrowserWindow;
public consoleBtn: Electron.TouchBarButton;
public startStopBtn: Electron.TouchBarButton;
public touchBar: Electron.TouchBar;
public versionSelector: Electron.TouchBarScrubber;
public versionSelectorBtn: Electron.TouchBarButton;
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.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.setupTouchBar();
}
/**
* This method is called right after we instantiate the manager. It sets up
* autoruns that respond to state changes (like new versions) and also
* greets the user with a kind little message.
*/
public setupTouchBar() {
// First, the welcome
this.setWelcomeItems();
// Then, go with the intro
setTimeout(() => {
this.setDefaultItems();
}, 4000);
autorun(this.updateStartStopBtn);
autorun(this.updateVersionSelectorItems);
autorun(this.updateVersionButton);
}
/**
* Whenever run/stop status changes, we'll update it (thanks, mobx)
*/
public updateStartStopBtn() {
const { label } = this.getStartStopButtonOptions();
this.startStopBtn.label = label!;
}
/**
* Whenever the electron versions change, we'll update
* the items in the scrubber (thanks, mobx)
*/
public updateVersionSelectorItems() {
this.versionSelector.items = this.getVersions();
}
/**
* Whenever the Electron version changes, update the version button
*/
public updateVersionButton() {
const { label } = this.getVersionButtonOptions();
this.versionSelectorBtn.label = label!;
}
public getVersions() {
return sortedElectronMap(this.appState.versions || {}, (key, version) => ({
label: `${getItemIcon(version)} ${key}`,
}));
}
/**
* Safely set the touch bar on the current window
*
* @param {Partial<Electron.TouchBarConstructorOptions>} options
*/
public setTouchBar(options: Partial<Electron.TouchBarConstructorOptions>) {
try {
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) {
console.warn(`Could not set touch bar`, { error });
}
}
/**
* A bar that just contains a little welcome message
*/
public setWelcomeItems() {
this.setTouchBar({
items: [
new TouchBarSpacer({ size: 'flexible' }),
new TouchBarLabel({ label: getNiceGreeting() }),
new TouchBarSpacer({ size: 'flexible' })
]
});
}
/**
* A bar with the default items
*/
public setDefaultItems() {
this.setTouchBar({
items: [
this.startStopBtn,
this.consoleBtn,
new TouchBarSpacer({ size: 'flexible' }),
this.versionSelectorBtn,
new TouchBarSpacer({ size: 'flexible' })
]
});
}
/**
* A bar that just contains the version scrubber
*/
public setVersionSelectorItems() {
this.setTouchBar({
items: [
this.versionSelector,
this.versionSelectorSelectBtn,
],
escapeItem: new TouchBarButton(
this.getVersionSelectorEscButtonOptions()
)
});
}
/**
* The version selector gets a custom "escape" button that'll
* take the user back to the "default" touch bar interface
*/
public getVersionSelectorEscButtonOptions() {
return {
label: `Back`,
click: () => {
this.setDefaultItems();
}
};
}
/**
* The version selector also gets a "select" button that'll
* commit the selected version
*/
public getVersionSelectorSelectButtonOptions() {
return {
label: `🎯 Select`,
click: () => {
if (this.selectedVersion) {
this.appState.setVersion(this.selectedVersion);
}
this.setDefaultItems();
}
};
}
/**
* The start button is wired up to launching the current
* fiddle.
*
* @returns {TouchBarButtonConstructorOptions}
*/
public getStartStopButtonOptions(): TouchBarButtonConstructorOptions {
const label = this.appState.isRunning
? '🛑 Stop'
: '🚀 Run';
const click = () => this.appState.isRunning
? window.ElectronFiddle.app.runner.stop()
: window.ElectronFiddle.app.runner.run();
return { label, click };
}
/**
* The console button is wired up to toggle the console
*
* @returns {TouchBarButtonConstructorOptions}
*/
public getConsoleButtonOptions(): TouchBarButtonConstructorOptions {
const label = '🖥 Console';
const click = this.appState.toggleConsole;
return { label, click };
}
/**
* The version button opens the scrubber, allowing selection
* of a version
*
* @returns {TouchBarButtonConstructorOptions}
*/
public getVersionButtonOptions(): TouchBarButtonConstructorOptions {
const label = `🔖 Electron ${this.appState.version}`;
const click = () => this.setVersionSelectorItems();
return { label, click };
}
/**
* Options for the version selection scrubber
*
* @returns {TouchBarScrubberConstructorOptions}
*/
public getVersionSelectorOptions(
): TouchBarScrubberConstructorOptions {
return {
select: this.selectVersion,
highlight: this.selectVersion,
items: [],
selectedStyle: 'background',
overlayStyle: 'outline',
mode: 'free',
continuous: true,
showArrowButtons: false
};
}
/**
* Called once the version selection scrubber is closed (
* with confirmation)
*
* @param {number} index
*/
public selectVersion(index: number) {
const versions = sortedElectronMap(this.appState.versions || {}, (k) => k);
this.selectedVersion = versions[index];
}
}
+5 -5
View File
@@ -15,11 +15,11 @@ export async function forgeTransform(files: Files): Promise<Files> {
// devDependencies
parsed.devDependencies = parsed.devDependencies || {};
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.22';
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.22';
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.22';
parsed.devDependencies['@electron-forge/maker-squirrel'] = '6.0.0-beta.22';
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.22';
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 || {};
+77 -25
View File
@@ -1,4 +1,4 @@
import { ElectronVersion, ElectronVersionSource, ElectronVersionState, NpmVersion } from '../interfaces';
import { ElectronVersion, ElectronVersionSource, NodeVersion, NpmVersion, VersionState } from '../interfaces';
import { normalizeVersion } from '../utils/normalize-version';
export const enum ElectronReleaseChannel {
@@ -45,13 +45,14 @@ export function getDefaultVersion(
* Return the release channel for a given input
* version.
*
* @param {NpmVersion} input
* @param {NpmVersion | string} input
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: NpmVersion
input: NpmVersion | string
): ElectronReleaseChannel {
const tag = input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
@@ -81,7 +82,7 @@ export const enum VersionKeys {
* @param {() => Array<NpmVersion>} fallbackMethod
* @returns {Array<NpmVersion>}
*/
function getVersions(
function getElectronVersions(
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
): Array<NpmVersion> {
const fromLs = window.localStorage.getItem(key);
@@ -116,7 +117,7 @@ function getVersions(
* @param {VersionKeys} key
* @param {Array<NpmVersion} versions
*/
function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
function saveElectronVersions(key: VersionKeys, versions: Array<NpmVersion>) {
const stringified = JSON.stringify(versions);
window.localStorage.setItem(key, stringified);
}
@@ -126,12 +127,12 @@ function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
*
* @returns {Array<NpmVersion>}
*/
export function getElectronVersions(): Array<ElectronVersion> {
export function getAllElectronVersions(): Array<ElectronVersion> {
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
return {
...version,
source: ElectronVersionSource.remote,
state: ElectronVersionState.unknown
state: VersionState.unknown
};
});
@@ -139,11 +140,11 @@ export function getElectronVersions(): Array<ElectronVersion> {
return {
...version,
source: ElectronVersionSource.local,
state: ElectronVersionState.ready
state: VersionState.ready
};
});
return [ ...known, ...local ];
return [...known, ...local];
}
/**
@@ -170,7 +171,7 @@ export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
* @returns {Array<NpmVersion>}
*/
export function getLocalVersions(): Array<NpmVersion> {
const versions = getVersions(VersionKeys.local, () => []);
const versions = getElectronVersions(VersionKeys.local, () => []);
return versions;
}
@@ -189,7 +190,7 @@ export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>)
return true;
});
return saveVersions(VersionKeys.local, filteredVersions);
return saveElectronVersions(VersionKeys.local, filteredVersions);
}
/**
@@ -199,7 +200,7 @@ export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>)
* @returns {Array<NpmVersion>}
*/
export function getKnownVersions(): Array<NpmVersion> {
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
return getElectronVersions(VersionKeys.known, () => require('../../static/releases.json'));
}
/**
@@ -208,7 +209,7 @@ export function getKnownVersions(): Array<NpmVersion> {
* @param {Array<NpmVersion>} versions
*/
export function saveKnownVersions(versions: Array<NpmVersion>) {
return saveVersions(VersionKeys.known, versions);
return saveElectronVersions(VersionKeys.known, versions);
}
/**
@@ -221,27 +222,78 @@ export function saveKnownVersions(versions: Array<NpmVersion>) {
export async function getUpdatedElectronVersions(
): Promise<Array<ElectronVersion>> {
try {
await fetchVersions();
await fetchElectronVersions();
} catch (error) {
console.warn(`Versions: Failed to fetch versions`, { error });
console.warn(`Versions: Failed to fetch Electron versions`, { error });
}
return getElectronVersions();
return getAllElectronVersions();
}
export async function getUpdatedNodeVersions(): Promise<Array<NodeVersion>> {
try {
return await fetchNodeVersions();
} catch (error) {
console.warn(`Versions: Failed to fetch Node.js versions`, { error });
}
return [];
}
/**
* Fetch the latest known versions directly from npm.
* Fetch the latest known Node.js versions directly from Node.js.
*
* @returns {Promise<Array<NpmVersion>>}
*
* let entry = {
* "headers": 1,
* "linux-arm64": 2,
* "linux-x64": 6,
* "osx-x64-tar": 8,
* "win-x64-zip": 14,
* "win-x86-zip": 18
* }
*
* nodejs.org/dist/${version}/node-${version}-${files[entry]}.file-ext
* ^ for windows, take the plat and extend with the file extension in the identifier above
*/
export async function fetchNodeVersions() {
const channels = [
`https://nodejs.org/dist/index.json`
];
let output: Array<NodeVersion> = [];
for (const channelUrl of channels) {
const response = await window.fetch(channelUrl);
const versions = await response.json();
output = versions.map((input: any) => ({
version: normalizeVersion(input.version)
}));
}
return output;
}
/**
* Fetch the latest known Electron versions directly from npm.
*
* @returns {Promise<Array<NpmVersion>>}
*/
export async function fetchVersions() {
const response = await window.fetch(`https://registry.npmjs.org/electron`);
const data = await response.json();
const versions: Record<string, any> = data.versions;
export async function fetchElectronVersions() {
const channels = [
`https://registry.npmjs.org/electron`, // stable, beta
`https://registry.npmjs.org/electron-nightly` // nightly
];
const output: Array<NpmVersion> = Object
.keys(versions)
.map((version) => ({ version }));
const output: Array<NpmVersion> = [];
for (const channelUrl of channels) {
const response = await window.fetch(channelUrl);
const data = await response.json();
const versions: Record<string, any> = data.versions;
output.push(...Object.keys(versions).map((version) => ({ version })));
}
if (output && output.length > 0 && isExpectedFormat(output)) {
console.log(`Fetched new Electron versions (Count: ${output.length})`);
+1
View File
@@ -1,4 +1,5 @@
export const INDEX_HTML_NAME = 'index.html';
export const MAIN_JS_NAME = 'main.js';
export const RENDERER_JS_NAME = 'renderer.js';
export const PRELOAD_JS_NAME = 'preload.js';
export const PACKAGE_NAME = 'package.json';
+4 -4
View File
@@ -1,4 +1,4 @@
import { ElectronVersion } from '../interfaces';
import { Version } from '../interfaces';
import { normalizeVersion } from './normalize-version';
/**
@@ -8,9 +8,9 @@ import { normalizeVersion } from './normalize-version';
* @param {Array<ElectronVersion>} input
* @returns {Record<string, ElectronVersion>}
*/
export function arrayToStringMap(
input: Array<ElectronVersion>
): Record<string, ElectronVersion> {
export function arrayToStringMap<T extends Version>(
input: Array<T>
): Record<string, T> {
const output = {};
input.forEach((version) => {
+13
View File
@@ -0,0 +1,13 @@
/**
* Get all the documentation urls for a given module name.
*
* @export
* @param {string} moduleName
*/
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}`
};
}
+26
View File
@@ -0,0 +1,26 @@
import { editor } from 'monaco-editor';
import { EditorId } from '../interfaces';
import { getEditorModel } from './editor-model';
import { getEditorValue } from './editor-value';
import { getEditorViewState } from './editor-viewstate';
export interface EditorBackup {
value: string;
model: editor.ITextModel | null;
viewState: editor.ICodeEditorViewState | null;
}
/**
* Returns a backup for a given editor
*
* @param {EditorId} id
* @returns {EditorBackup}
*/
export function getEditorBackup(id: EditorId): EditorBackup {
return {
value: getEditorValue(id),
model: getEditorModel(id),
viewState: getEditorViewState(id)
};
}
+5 -5
View File
@@ -3,11 +3,11 @@
* a debounced version below.
*/
function _updateEditorLayout() {
const { main, renderer, html } = window.ElectronFiddle.editors;
if (main) main.layout();
if (renderer) renderer.layout();
if (html) html.layout();
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
if (window.ElectronFiddle.editors[key]) {
window.ElectronFiddle.editors[key].layout();
}
});
}
let handle: number;
+23
View File
@@ -0,0 +1,23 @@
import { editor } from 'monaco-editor';
import { EditorId } from '../interfaces';
/**
* Return the model for a given editor
*
* @param {EditorId} id
* @returns {editor.ITextModel | null}
*/
export function getEditorModel(id: EditorId): editor.ITextModel | null {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (fiddle.editors[id] && fiddle.editors[id]!.getModel) {
return fiddle.editors[id]!.getModel();
}
return null;
}
+21
View File
@@ -0,0 +1,21 @@
import { EditorId } from '../interfaces';
/**
* Return the value for a given editor
*
* @param {EditorId} id
* @returns {string}
*/
export function getEditorValue(id: EditorId): string {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (fiddle.editors[id] && fiddle.editors[id]!.getValue) {
return fiddle.editors[id]!.getValue();
}
return '';
}
+24
View File
@@ -0,0 +1,24 @@
import { editor } from 'monaco-editor';
import { EditorId } from '../interfaces';
/**
* Returns the view state for a given editor.
*
* @export
* @param {EditorId} id
* @returns {(editor.ICodeEditorViewState | null)}
*/
export function getEditorViewState(id: EditorId): editor.ICodeEditorViewState | null {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (fiddle.editors[id] && fiddle.editors[id]!.saveViewState) {
return fiddle.editors[id]!.saveViewState();
}
return null;
}
+57
View File
@@ -0,0 +1,57 @@
import { MosaicDirection, MosaicNode } from 'react-mosaic-component';
import { MosaicId } from '../interfaces';
/**
* Create a mosaic arrangement given an array of editor ids.
*
* @export
* @param {Array<MosaicId>} input
* @returns {MosaicNode<MosaicId>}
*/
export function createMosaicArrangement(
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 firstHalf = secondHalf.splice(0, Math.floor(secondHalf.length / 2));
return {
direction,
first: createMosaicArrangement(firstHalf, 'column'),
second: createMosaicArrangement(secondHalf, 'column')
};
}
/**
* Returns an array of visible editors given a mosaic arrangement.
*
* @export
* @param {MosaicNode<MosaicId> | null} input
* @returns {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 ];
}
// Handle the other cases (2 - 4)
const result = [];
for (const node of [ input.first, input.second ]) {
if (typeof node === 'string') {
result.push(node);
} else {
result.push(...getVisibleMosaics(node));
}
}
return result;
}
+25
View File
@@ -0,0 +1,25 @@
import { getRandomNumber } from './random';
/**
* Get a random nice greeting
*
* @export
* @returns
*/
export function getNiceGreeting() {
const greetings = [
'👋 Go build a great app',
'💖 You got this',
`💖 We think you're great`,
'🤘 Your app will rock, we know it',
'🙇‍♀️ Thanks for trying out Electron & Fiddle',
'🐕 Go pet a dog today',
'🐈 Go pet a cat today',
'💧 Stay hydrated',
];
const min = 0;
const max = greetings.length;
return greetings[getRandomNumber(min, max)];
}
+18 -3
View File
@@ -1,15 +1,30 @@
import * as GitHubType from '@octokit/rest';
import { AppState } from '../renderer/state';
let _Octokit: typeof GitHubType;
let _octo: GitHubType;
/**
* Returns a loaded Octokit.
* Returns a loaded Octokit. If state is passed and authentication
* is available, we'll token-authenticate.
*
* @export
* @returns {Promise<typeof GitHubType>}
*/
export async function getOctokit(): Promise<GitHubType> {
export async function getOctokit(
appState?: AppState
): Promise<GitHubType> {
_Octokit = _Octokit || (await import('@octokit/rest') as any).default;
return _octo = _octo || new _Octokit();
_octo = _octo || new _Octokit();
// You can load Gists without being authenticated,
// but we get better rate limits when authenticated
if (appState && appState.gitHubToken) {
_octo.authenticate({
type: 'token',
token: appState.gitHubToken!
});
}
return _octo;
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Dumb helper: Get a random number between two numbers
*
* @param {number} [min=0]
* @param {number} [max=1]
* @returns {number}
*/
export function getRandomNumber(
min: number = 0, max: number = 1
): number {
return Math.floor(Math.random() * max) + min;
}
+28
View File
@@ -0,0 +1,28 @@
import { AboutPanelOptionsOptions, app } from 'electron';
/**
* Sets Fiddle's About panel options on Linux and macOS
*
* @returns
*/
export function setupAboutPanel(): void {
if (process.platform === 'win32') return;
const options: AboutPanelOptionsOptions = {
applicationName: 'Electron Fiddle',
applicationVersion: app.getVersion(),
version: process.versions.electron,
copyright: '© Electron Authors',
};
switch (process.platform) {
case 'linux':
options.website = 'https://electronjs.org/fiddle';
case 'darwin':
options.credits = 'https://github.com/electron/fiddle/graphs/contributors';
default:
// fallthrough
}
app.setAboutPanelOptions(options);
}
@@ -1,6 +1,6 @@
import * as semver from 'semver';
import { ElectronVersion } from '../interfaces';
import { ElectronVersion, NodeVersion } from '../interfaces';
/**
* Sorts Electron versions and returns the result of a
@@ -13,6 +13,28 @@ import { ElectronVersion } from '../interfaces';
export function sortedElectronMap<T>(
versions: Record<string, ElectronVersion>,
mapFn: (key: string, version: ElectronVersion) => T
) {
return sortedMap<ElectronVersion, T>(versions, mapFn);
}
/**
* Sorts Node versions and returns the result of a
* map function.
*
* @param {Record<string, NodeVersion>} versions
* @param {(key: string, version: NodeVersion) => void} mapFn
* @returns {Array<T>}
*/
export function sortedNodeMap<T>(
versions: Record<string, NodeVersion>,
mapFn: (key: string, version: NodeVersion) => T
) {
return sortedMap<NodeVersion, T>(versions, mapFn);
}
function sortedMap<T1, T2>(
versions: Record<string, T1>,
mapFn: (key: string, version: T1) => T2
) {
return Object.keys(versions)
.sort((a, b) => {
@@ -30,5 +52,5 @@ export function sortedElectronMap<T>(
return semver.gt(a, b, true) ? -1 : 1;
})
.map((key) => mapFn(key, versions[key])) as Array<T>;
.map((key) => mapFn(key, versions[key])) as Array<T2>;
}
+35
View File
@@ -0,0 +1,35 @@
import { ALL_EDITORS, EditorId, MosaicId, PanelId } from '../interfaces';
import { EditorBackup } from './editor-backup';
/**
* Is the given string of type `EditorId`?
*
* @export
* @param {(EditorId | PanelId | MosaicId)} input
* @returns {input is EditorId}
*/
export function isEditorId(input: EditorId | PanelId | MosaicId): input is EditorId {
return (ALL_EDITORS as any).includes(input);
}
/**
* Is the given string of type `isPanelId`?
*
* @export
* @param {(EditorId | PanelId | MosaicId)} input
* @returns {input is MosaicId}
*/
export function isPanelId(input: EditorId | PanelId | MosaicId): input is PanelId {
return !(ALL_EDITORS as any).includes(input);
}
/**
* Is the given input of type `EditorBackup`?
*
* @export
* @param {(EditorBackup | true)} input
* @returns {input is EditorBackup}
*/
export function isEditorBackup(input?: EditorBackup | true | null): input is EditorBackup {
return !!(input && input !== true);
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
+1 -1
View File
@@ -1 +1 @@
[{"version":"0.1.0"},{"version":"0.1.1"},{"version":"0.1.2"},{"version":"0.2.0"},{"version":"0.2.1"},{"version":"0.3.0"},{"version":"0.4.0"},{"version":"0.4.1"},{"version":"1.3.1"},{"version":"1.3.2"},{"version":"1.3.3"},{"version":"1.3.4"},{"version":"1.3.5"},{"version":"1.3.6"},{"version":"1.4.0"},{"version":"1.4.1"},{"version":"1.3.7"},{"version":"1.4.2"},{"version":"1.4.3"},{"version":"1.4.4"},{"version":"1.3.8"},{"version":"1.4.5"},{"version":"1.4.6"},{"version":"1.4.7"},{"version":"1.3.9"},{"version":"1.3.10"},{"version":"1.4.8"},{"version":"1.3.12"},{"version":"1.4.10"},{"version":"1.3.13"},{"version":"1.4.11"},{"version":"1.4.12"},{"version":"1.4.13"},{"version":"1.4.14"},{"version":"1.4.15"},{"version":"1.5.0"},{"version":"1.5.1"},{"version":"1.6.0"},{"version":"1.6.1"},{"version":"1.6.2"},{"version":"1.6.3"},{"version":"1.3.14"},{"version":"1.6.4"},{"version":"1.6.5"},{"version":"1.4.16"},{"version":"1.6.6"},{"version":"1.6.7"},{"version":"1.3.15"},{"version":"1.6.8"},{"version":"1.6.9"},{"version":"1.7.0"},{"version":"1.6.10"},{"version":"1.7.1"},{"version":"1.6.11"},{"version":"1.7.2"},{"version":"1.7.3"},{"version":"1.7.4"},{"version":"1.7.5"},{"version":"1.6.12"},{"version":"1.7.6"},{"version":"1.7.7"},{"version":"1.6.13"},{"version":"1.7.8"},{"version":"1.6.14"},{"version":"1.8.1"},{"version":"1.7.9"},{"version":"1.6.15"},{"version":"1.8.2-beta.1"},{"version":"1.8.2-beta.2"},{"version":"1.8.2-beta.3"},{"version":"1.7.10"},{"version":"1.7.11"},{"version":"1.6.16"},{"version":"1.8.2-beta.4"},{"version":"1.8.2-beta.5"},{"version":"1.7.12"},{"version":"1.6.17"},{"version":"1.8.2"},{"version":"2.0.0-beta.1"},{"version":"2.0.0-beta.2"},{"version":"1.8.3"},{"version":"2.0.0-beta.3"},{"version":"1.7.13"},{"version":"2.0.0-beta.4"},{"version":"1.8.4"},{"version":"2.0.0-beta.5"},{"version":"2.0.0-beta.6"},{"version":"2.0.0-beta.7"},{"version":"2.0.0-beta.8"},{"version":"1.8.5"},{"version":"1.7.14"},{"version":"1.8.6"},{"version":"2.0.0"},{"version":"1.6.18"},{"version":"1.7.15"},{"version":"1.8.7"},{"version":"2.0.1"},{"version":"2.0.2"},{"version":"3.0.0-beta.1"},{"version":"2.0.3"},{"version":"2.0.4"},{"version":"3.0.0-beta.2"},{"version":"2.0.5"},{"version":"3.0.0-beta.3"},{"version":"2.0.6"},{"version":"3.0.0-beta.4"},{"version":"2.0.7"},{"version":"2.1.0-unsupported.20180809"},{"version":"3.0.0-beta.5"},{"version":"4.0.0-nightly.20180816"},{"version":"4.0.0-nightly.20180817"},{"version":"3.0.0-nightly.20180818"},{"version":"4.0.0-nightly.20180819"},{"version":"2.0.8-nightly.20180819"},{"version":"3.0.0-beta.6"},{"version":"2.0.8-nightly.20180820"},{"version":"3.0.0-nightly.20180821"},{"version":"4.0.0-nightly.20180821"},{"version":"3.0.0-beta.7"},{"version":"2.0.8"},{"version":"1.8.8"},{"version":"1.7.16"},{"version":"3.0.0-beta.8"},{"version":"3.0.0-nightly.20180904"},{"version":"3.0.0-beta.9"},{"version":"3.0.0-beta.10"},{"version":"2.0.9"},{"version":"3.0.0-beta.11"},{"version":"3.0.0-beta.12"},{"version":"3.0.0-beta.13"},{"version":"3.0.0"},{"version":"2.0.10"},{"version":"3.0.1"},{"version":"3.0.2"},{"version":"4.0.0-nightly.20180929"},{"version":"2.0.11"},{"version":"4.0.0-nightly.20181006"},{"version":"3.0.3"},{"version":"4.0.0-nightly.20181010"},{"version":"3.0.4"},{"version":"4.0.0-beta.1"},{"version":"4.0.0-beta.2"},{"version":"4.0.0-beta.3"},{"version":"3.0.5"},{"version":"2.0.12"},{"version":"4.0.0-beta.4"},{"version":"4.0.0-beta.5"},{"version":"3.0.6"},{"version":"3.0.7"},{"version":"2.0.13"},{"version":"4.0.0-beta.6"},{"version":"3.0.8"},{"version":"4.0.0-beta.7"},{"version":"3.0.9"},{"version":"2.0.14"},{"version":"3.0.10"},{"version":"4.0.0-beta.8"},{"version":"3.1.0-beta.1"},{"version":"3.1.0-beta.2"},{"version":"3.0.11"},{"version":"4.0.0-beta.9"},{"version":"2.0.15"},{"version":"3.0.12"},{"version":"3.1.0-beta.3"},{"version":"3.0.13"},{"version":"3.1.0-beta.4"},{"version":"4.0.0-beta.10"}]
[{"version":"0.1.0"},{"version":"0.1.1"},{"version":"0.1.2"},{"version":"0.2.0"},{"version":"0.2.1"},{"version":"0.3.0"},{"version":"0.4.0"},{"version":"0.4.1"},{"version":"1.3.1"},{"version":"1.3.2"},{"version":"1.3.3"},{"version":"1.3.4"},{"version":"1.3.5"},{"version":"1.3.6"},{"version":"1.4.0"},{"version":"1.4.1"},{"version":"1.3.7"},{"version":"1.4.2"},{"version":"1.4.3"},{"version":"1.4.4"},{"version":"1.3.8"},{"version":"1.4.5"},{"version":"1.4.6"},{"version":"1.4.7"},{"version":"1.3.9"},{"version":"1.3.10"},{"version":"1.4.8"},{"version":"1.3.12"},{"version":"1.4.10"},{"version":"1.3.13"},{"version":"1.4.11"},{"version":"1.4.12"},{"version":"1.4.13"},{"version":"1.4.14"},{"version":"1.4.15"},{"version":"1.5.0"},{"version":"1.5.1"},{"version":"1.6.0"},{"version":"1.6.1"},{"version":"1.6.2"},{"version":"1.6.3"},{"version":"1.3.14"},{"version":"1.6.4"},{"version":"1.6.5"},{"version":"1.4.16"},{"version":"1.6.6"},{"version":"1.6.7"},{"version":"1.3.15"},{"version":"1.6.8"},{"version":"1.6.9"},{"version":"1.7.0"},{"version":"1.6.10"},{"version":"1.7.1"},{"version":"1.6.11"},{"version":"1.7.2"},{"version":"1.7.3"},{"version":"1.7.4"},{"version":"1.7.5"},{"version":"1.6.12"},{"version":"1.7.6"},{"version":"1.7.7"},{"version":"1.6.13"},{"version":"1.7.8"},{"version":"1.6.14"},{"version":"1.8.1"},{"version":"1.7.9"},{"version":"1.6.15"},{"version":"1.8.2-beta.1"},{"version":"1.8.2-beta.2"},{"version":"1.8.2-beta.3"},{"version":"1.7.10"},{"version":"1.7.11"},{"version":"1.6.16"},{"version":"1.8.2-beta.4"},{"version":"1.8.2-beta.5"},{"version":"1.7.12"},{"version":"1.6.17"},{"version":"1.8.2"},{"version":"2.0.0-beta.1"},{"version":"2.0.0-beta.2"},{"version":"1.8.3"},{"version":"2.0.0-beta.3"},{"version":"1.7.13"},{"version":"2.0.0-beta.4"},{"version":"1.8.4"},{"version":"2.0.0-beta.5"},{"version":"2.0.0-beta.6"},{"version":"2.0.0-beta.7"},{"version":"2.0.0-beta.8"},{"version":"1.8.5"},{"version":"1.7.14"},{"version":"1.8.6"},{"version":"2.0.0"},{"version":"1.6.18"},{"version":"1.7.15"},{"version":"1.8.7"},{"version":"2.0.1"},{"version":"2.0.2"},{"version":"3.0.0-beta.1"},{"version":"2.0.3"},{"version":"2.0.4"},{"version":"3.0.0-beta.2"},{"version":"2.0.5"},{"version":"3.0.0-beta.3"},{"version":"2.0.6"},{"version":"3.0.0-beta.4"},{"version":"2.0.7"},{"version":"2.1.0-unsupported.20180809"},{"version":"3.0.0-beta.5"},{"version":"4.0.0-nightly.20180816"},{"version":"4.0.0-nightly.20180817"},{"version":"3.0.0-nightly.20180818"},{"version":"4.0.0-nightly.20180819"},{"version":"2.0.8-nightly.20180819"},{"version":"3.0.0-beta.6"},{"version":"2.0.8-nightly.20180820"},{"version":"3.0.0-nightly.20180821"},{"version":"4.0.0-nightly.20180821"},{"version":"3.0.0-beta.7"},{"version":"2.0.8"},{"version":"1.8.8"},{"version":"1.7.16"},{"version":"3.0.0-beta.8"},{"version":"3.0.0-nightly.20180904"},{"version":"3.0.0-beta.9"},{"version":"3.0.0-beta.10"},{"version":"2.0.9"},{"version":"3.0.0-beta.11"},{"version":"3.0.0-beta.12"},{"version":"3.0.0-beta.13"},{"version":"3.0.0"},{"version":"2.0.10"},{"version":"3.0.1"},{"version":"3.0.2"},{"version":"4.0.0-nightly.20180929"},{"version":"2.0.11"},{"version":"4.0.0-nightly.20181006"},{"version":"3.0.3"},{"version":"4.0.0-nightly.20181010"},{"version":"3.0.4"},{"version":"4.0.0-beta.1"},{"version":"4.0.0-beta.2"},{"version":"4.0.0-beta.3"},{"version":"3.0.5"},{"version":"2.0.12"},{"version":"4.0.0-beta.4"},{"version":"4.0.0-beta.5"},{"version":"3.0.6"},{"version":"3.0.7"},{"version":"2.0.13"},{"version":"4.0.0-beta.6"},{"version":"3.0.8"},{"version":"4.0.0-beta.7"},{"version":"3.0.9"},{"version":"2.0.14"},{"version":"3.0.10"},{"version":"4.0.0-beta.8"},{"version":"3.1.0-beta.1"},{"version":"3.1.0-beta.2"},{"version":"3.0.11"},{"version":"4.0.0-beta.9"},{"version":"2.0.15"},{"version":"3.0.12"},{"version":"3.1.0-beta.3"},{"version":"3.0.13"},{"version":"3.1.0-beta.4"},{"version":"4.0.0-beta.10"},{"version":"2.0.16"},{"version":"4.0.0-beta.11"},{"version":"4.0.0"},{"version":"3.1.0-beta.5"},{"version":"4.0.1"},{"version":"3.0.14"},{"version":"3.1.0"},{"version":"3.1.1"},{"version":"4.0.2"},{"version":"5.0.0-beta.1"},{"version":"3.1.2"},{"version":"4.0.3"},{"version":"2.0.17"},{"version":"3.0.15"},{"version":"3.1.3"},{"version":"4.0.4"},{"version":"5.0.0-beta.2"},{"version":"4.0.5"},{"version":"5.0.0-beta.3"},{"version":"3.1.4"},{"version":"4.0.6"},{"version":"5.0.0-beta.4"},{"version":"3.1.5"},{"version":"4.0.7"},{"version":"5.0.0-beta.5"},{"version":"2.0.18"},{"version":"3.1.6"},{"version":"3.0.16"},{"version":"4.0.8"},{"version":"4.1.0"},{"version":"5.0.0-beta.6"},{"version":"4.1.1"},{"version":"3.1.7"},{"version":"5.0.0-beta.7"}]

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