Compare commits

...
626 Commits
Author SHA1 Message Date
Mark Lee 0c06a33018 build(deps-dev): upgrade electron-forge to ^6.0.0-beta.52 2020-07-08 00:37:52 -07:00
Mark Lee af4155b2df Merge branch 'master' into circleci 2020-07-08 00:33:41 -07:00
Erick Zhao 2a67e7e6cf fix: consistent app quit dialog behaviour (#423) 2020-07-07 12:21:15 -07:00
Vlad Hashimoto 351c5caf23 feat: set the appData dir from the main process (#430) 2020-07-06 23:31:34 +03:00
github-actions[bot]andmalept 36780a95e7 build: update Electron releases JSON (#431)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-07-06 03:59:28 +03:00
Shelley Vohr 9065a8678a fix: audio/video entitlements crash (#426)
* fix: add entitlements for audio and video capture

* chore: add usageDescription

* empty commit
2020-07-01 12:49:39 -07:00
github-actions[bot]andmalept 016d3ee78a build: update Electron releases JSON (#419)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-06-29 20:02:40 +03:00
Shelley Vohr 4b40420bba feat: log notification info in adv logging (#421) 2020-06-25 11:30:28 -07:00
Vlad Hashimoto 0e872b0dd0 feat: allow to publish if user would to do it (#413) 2020-06-18 15:11:06 +03:00
github-actions[bot]andmalept 7638111598 build: update Electron releases JSON (#414)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-06-16 08:14:39 -07:00
Benjamin Sova a0e59ad288 docs: fix explanation of darwin auto-quit prevention (#410) 2020-06-11 09:48:38 -07:00
Erick Zhao 3c6103dc50 chore: 0.15.1 (#409) 2020-06-09 10:24:32 -07:00
Erick Zhao b98472f973 fix: make hidden editors interact properly with remote content (#404) 2020-06-08 16:37:20 -07:00
Jeremy Rose 3602eacc88 fix: correct sense of bisect buttons (#408) 2020-06-08 16:19:54 -07:00
github-actions[bot] 8b84c040c7 build: update Electron releases JSON (#399) 2020-06-07 20:36:37 -07:00
Erick Zhao 6aa19eada6 fix: proper async/await usage in editor.tsx (#403) 2020-06-01 14:39:41 -07:00
Erick Zhao 8e92183de2 refactor: replace app.getName() with app.name (#401) 2020-05-27 16:03:17 -07:00
Vlad Hashimoto 1a951f3f44 feat: v0.15.0 (#398) 2020-05-23 02:46:28 +03:00
Will Anderson 66ead93f05 fix: Update documentation link for user-provided flags (#397) 2020-05-23 00:26:09 +03:00
Vlad Hashimoto d22899b4a5 feat: add ability to update gist instead of publishing (#386) 2020-05-22 23:59:27 +03:00
Vlad Hashimoto 77d4bc6fc5 fix: use role: quit for render exit button in menu (#395) 2020-05-21 00:45:41 +03:00
github-actions[bot] 961d42d74f build: update Electron releases JSON (#393) 2020-05-20 21:33:13 +03:00
Vlad Hashimoto c58ade2132 fix: reset github token when authentication failed (#388) 2020-05-20 21:31:00 +03:00
github-actions[bot] 592c220f18 build: update Electron releases JSON (#391) 2020-05-13 21:13:51 +03:00
Vlad Hashimoto 4da0e82e9b fix: set the window to null after destroying (#389) 2020-05-13 20:54:18 +03:00
Vlad Hashimoto f766bef10c fix: doesn't error when unable to watch file (#390) 2020-05-12 19:36:28 -07:00
Natalie 30ba9fb023 docs: improve CONTRIBUTING.md (#380)
This PR adds a Contributing section to the README and descriptive text for running Fiddle from source in CONTRIBUTING.md.
2020-05-12 08:53:17 -07:00
github-actions[bot] f3e847da85 build: update Electron releases JSON (#381) 2020-05-04 23:42:14 -07:00
Kimberlee Johnson 0f854441e4 feat: reset layout button (#374) 2020-04-29 17:35:18 -07:00
Vlad Hashimoto f31a4e38bb fix: don't modify state directly (#385) 2020-04-28 14:18:18 -07:00
Felix Rieseberg 26bb3e4944 Merge pull request #373 from codebytere/update-electron-get
chore: update @electron/get version
2020-04-21 11:03:20 -07:00
Vlad Hashimoto fb1fb7869c Merge pull request #376 from electron/update-releases
build: update Electron releases JSON
2020-04-20 12:21:15 +03:00
malept a1e67400bf build: update Electron releases JSON 2020-04-20 00:04:55 +00:00
Shelley Vohr a26aa1e70c chore: update @electron/get version 2020-04-14 14:22:25 -07:00
github-actions[bot]andElectron Bot 78395412f4 build: update Electron releases JSON (#368)
Co-authored-by: Electron Bot <electron-bot@users.noreply.github.com>
2020-04-13 07:51:52 -07:00
Felix Rieseberg 1f72db3cfc 0.14.0 2020-04-06 10:22:19 -07:00
Felix Rieseberg c333a1e132 Merge pull request #366 from electron/update-releases
build: update Electron releases JSON
2020-04-06 10:19:49 -07:00
malept a0a989d7a7 build: update Electron releases JSON 2020-04-06 00:13:17 +00:00
github-actions[bot]andmalept 5b07b8a706 build: update Electron releases JSON (#364)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-03-30 03:20:17 +03:00
Felix Rieseberg 98a358b756 Merge pull request #358 from electron/download-progress
Refactor BinaryManager, add download progress indicator
2020-03-24 11:14:55 -07:00
Mark Lee 548b68e208 Remove config files for AppVeyor & Travis CI 2020-03-23 15:04:44 -07:00
Mark Lee 683def4215 Use Debian Buster to get a recent RPM version 2020-03-23 15:01:57 -07:00
Mark Lee 5596c12f19 Remove resolution 2020-03-23 15:01:57 -07:00
Mark Lee db9c1d5105 Remove unused steps 2020-03-23 15:01:57 -07:00
Mark Lee b52e70ec61 publish conflicts with yarn's publish 2020-03-23 15:01:57 -07:00
Mark Lee 305bb9afbe Fix syntax 2020-03-23 15:01:57 -07:00
Mark Lee d121954018 Add a fake GitHub token for publish dry run 2020-03-23 15:01:57 -07:00
Mark Lee e51b1648cb Double-dash not necessary with yarn 2020-03-23 15:01:57 -07:00
Mark Lee ccc72ee70a Use yarn instead of npm in CI config 2020-03-23 15:01:57 -07:00
Samuel Attard 48c62f4e02 build: circleci 2020-03-23 15:01:57 -07:00
Felix Rieseberg 9b55b2ef26 Merge pull request #357 from electron/no-notarize
build: Only notarize during make
2020-03-23 10:45:16 -07:00
Felix Rieseberg 5b91177348 Merge pull request #359 from electron/update-releases
build: update Electron releases JSON
2020-03-23 10:44:51 -07:00
malept f8c43a07f7 build: update Electron releases JSON 2020-03-23 00:04:50 +00:00
Felix Rieseberg 5eba9cdbe2 feat: A fancy spinner for downloading 2020-03-17 18:11:05 -07:00
Felix Rieseberg 8ee42c290e test: Fix the tests post-refactor 2020-03-17 17:58:25 -07:00
Felix Rieseberg c2b3954dfa chore: Refactor BinaryManager, no more classes 2020-03-17 17:07:25 -07:00
Felix Rieseberg e61599e9c5 chore: Better named interfaces 2020-03-17 16:24:58 -07:00
Felix RiesebergandMark Lee 1fed772a27 Update forge.config.js
Co-Authored-By: Mark Lee <malept@users.noreply.github.com>
2020-03-17 16:13:32 -07:00
Felix RiesebergandMark Lee de85d0f324 Update forge.config.js
Co-Authored-By: Mark Lee <malept@users.noreply.github.com>
2020-03-17 16:13:16 -07:00
Erick Zhao c89671d99d feat: improved bisect commands UX (#352) 2020-03-16 10:55:34 -07:00
Felix Rieseberg 155b901ade build: Only notarize during make 2020-03-16 10:47:00 -07:00
github-actions[bot]andmalept 8e098deab8 build: update Electron releases JSON (#356)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-03-16 02:18:36 +02:00
dependabot[bot] 2b5ea7d467 build(deps): bump acorn from 5.7.1 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.1 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.1...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-13 23:49:10 -07:00
Vlad Hashimoto 91efa5a009 v0.13.1 2020-03-13 16:45:08 -07:00
Erick Zhao 4ec5bec719 fix: load unsave handlers only after editors mount (#351) 2020-03-12 11:53:29 -07:00
Erick Zhao 84ac41f147 feat: more sensible version filtering (#344) 2020-03-12 11:46:32 -07:00
Erick Zhao e9a2985291 fix: preserve 'downloading' version state post-refresh (#345) 2020-03-12 11:46:24 -07:00
Erick ZhaoandFelix Rieseberg 9b28919203 feat: show inactive window on quit warning (#348)
Co-authored-by: Felix Rieseberg <felix@felixrieseberg.com>
2020-03-12 11:13:08 -07:00
Erick Zhao 8d43836787 fix: ensure you have at least one editor open (#347) 2020-03-11 12:54:16 -07:00
Shelley Vohr 48d0dadf5d chore: fix test:report bailing (#349) 2020-03-11 12:32:03 -07:00
Erick Zhao 2f9ef21251 fix: ensure hidden panels work on first load (#346) 2020-03-11 12:12:09 -07:00
Erick Zhao 8e4238275c refactor: grab release list from unpkg (#339) 2020-03-09 11:45:54 -07:00
Shelley Vohr 32d5ea99aa chore: add test report generation helper (#340) 2020-03-09 09:28:43 -07:00
Felix Rieseberg da2e17cd37 Merge pull request #342 from electron/update-releases
build: update Electron releases JSON
2020-03-08 18:13:01 -07:00
malept 539f5bce97 build: update Electron releases JSON 2020-03-09 00:06:03 +00:00
Mark Lee 228552c8a0 build: create a PR for releases.json updates instead of trying to push to master (#341)
* Move expected version count & latest Electron version to its own
  fixture file
* Automatically update expected version count & latest Electron version
  when updating the JSON file
* Throw an error if a rejection occurs when the fetch releases script
  is run
* reduce releases update frequency to once a week
2020-03-06 16:17:42 -08:00
Felix Rieseberg ed8d76a54a fix: Sentry is a runtime dependency 2020-03-02 17:14:29 -08:00
Felix Rieseberg 2f4d47a0ab 0.13.0 2020-03-02 16:14:25 -08:00
Mark Lee 9b443de137 build: auto-update the Electron Releases JSON file every day (#331) 2020-03-02 16:08:39 -08:00
Felix Rieseberg efb5bf0e57 Merge pull request #334 from electron/travis-cleanup
build: cache/install node deps per Travis docs, remove deprecated option
2020-03-02 16:03:59 -08:00
Mark Lee c420747bb9 build: cache/install node dependencies per Travis docs, remove deprecated option 2020-03-02 15:40:23 -08:00
Felix Rieseberg 33223e7b37 Merge pull request #333 from electron/use-bionic
build: Use Ubuntu 18.04 in Travis CI
2020-03-02 15:35:04 -08:00
Felix Rieseberg 86cc0c62a1 build: Use Bionic 2020-03-02 14:50:40 -08:00
Felix Rieseberg 96bd2e36dd Merge pull request #330 from electron/update-dependencies
Update dependencies
2020-03-02 13:02:16 -08:00
Felix Rieseberg 9db527773b test: Fix the tests 2020-02-27 17:43:16 -08:00
Felix Rieseberg 9da1acafd9 wip 2020-02-27 16:56:51 -08:00
Felix Rieseberg 5a05d34776 chore: Update local Electron releases 2020-02-27 14:42:39 -08:00
Felix Rieseberg 6b9d2b66e2 wrench: Update dependencies 2020-02-27 14:41:44 -08:00
Felix Rieseberg d4096a6de8 Merge pull request #329 from electron/less-listeners
chore: Unregister various listeners
2020-02-26 12:28:55 -08:00
Erick Zhao 3709d122a6 feat: add CSS support (#327) 2020-02-22 11:48:11 -08:00
Felix Rieseberg 0368cda57a Merge pull request #328 from electron/fix/remove-reload
fix: remove broken "Reload" menu item
2020-02-22 08:53:34 -08:00
Felix Rieseberg 433cb6acd5 chore: Use node environment where available 2020-02-22 08:50:56 -08:00
Felix Rieseberg 56dceaefb0 chore: Don't collect coverage locally 2020-02-22 08:35:35 -08:00
Erick Zhao 80da23c4e4 fix: remove broken "Reload" menu item 2020-02-21 10:09:41 -08:00
Felix Rieseberg 353968ef8e Merge pull request #326 from electron/bisect-constraint
feat: Add some help to the bisect
2020-02-19 13:29:05 -08:00
Felix RiesebergandErick Zhao 64ece1b364 Apply suggestions from code review
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2020-02-19 12:29:20 -08:00
Felix Rieseberg 13383f7345 chore: Unregister various listeners 2020-02-15 20:50:57 -08:00
Felix Rieseberg cae7222636 feat: Add some help to the bisect 2020-02-15 20:11:08 -08:00
Felix Rieseberg 2aabe80f2b Merge pull request #324 from electron/fix-bisect
fix: Reorganize the bisect helper
2020-02-15 18:54:11 -08:00
Erick Zhao d0683f1392 fix: prevent other editors from clipping tooltips (#294) 2020-02-13 11:45:42 -08:00
Felix Rieseberg 389352cefa test: Get some of that coverage back 2020-02-12 20:50:18 -08:00
Felix Rieseberg d04b654c3e fix: Reorganize the bisect helper 2020-02-12 14:24:57 -08:00
Felix Rieseberg 1369c9b35c Merge pull request #318 from electron/sentry
chore: add sentry for crash / js error reporting
2020-02-11 07:56:12 -08:00
Samuel Attard 9957090d25 Update electron.ts 2020-02-05 15:14:25 -08:00
Samuel Attard 8a002fb0a8 chore: add sentry for crash / js error reporting 2020-02-05 13:15:02 -08:00
Mark Lee cae8dde4c8 refactor(show-me): avoid using a global mainWindow variable (#314) 2020-02-03 14:19:04 -08:00
Jeremy Apthorp cb176534f9 fix: stop holding a reference to mainWindow (#312) 2020-01-30 09:37:34 -08:00
Erick Zhao f7fa9a8ffc chore: merge pull request #306 from issacgerges/bisect-success-dialog
feat: bisect success dialog
2020-01-29 09:54:24 -08:00
Issac Gerges 347384a547 chore: improve GenericDialog test coverage 2020-01-28 19:22:58 -08:00
Issac Gerges e272efb3b9 fix: add quoted escapes to paths to prevent param expansion on zsh. Remove dead code. 2020-01-28 19:16:22 -08:00
Mark LeeandFelix Rieseberg 5255bcb842 chore: add contributing guide (#311)
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2020-01-28 18:51:54 -08:00
Felix Rieseberg d3cd53f3e0 Merge pull request #309 from electron/erick/pong-prints-pong
fix: print correct output in ipcRenderer show-me
2020-01-28 17:45:51 -08:00
Erick Zhao 6d28c4c81b fix: print correct output in ipcRender show-me 2020-01-28 14:55:56 -08:00
Issac Gerges a45354e1af chore: quote lint glob to prevent zsh to parse it as parameter expansion 2020-01-27 18:25:50 -08:00
Issac Gerges 28245d36ba refactor: rename DialogType 'message' to 'success' 2020-01-27 14:48:30 -08:00
Issac Gerges 582ef280b5 Merge remote-tracking branch 'upstream/master' into bisect-dialog 2020-01-27 13:00:48 -08:00
Issac Gerges 56b7f2ffb7 refactor: replace warning/confirmation dialogs with generic dialog and type parameter 2020-01-27 12:58:11 -08:00
issacgerges cb3ba4f0a7 chore: Add issacgerges as a contributor (#304) 2020-01-26 13:57:32 -08:00
Felix Rieseberg e238b10f1e Merge pull request #303 from issacgerges/fix-remote-warning
fix: Make example warning more descriptive
2020-01-26 08:59:37 -08:00
Felix Rieseberg bbc22d9ca8 Merge pull request #301 from issacgerges/support-original-fs
fix: Add original-fs to ignored modules
2020-01-26 08:58:34 -08:00
issacgerges 7b3c960c1f chore: fix autoUpdater example to remove spurious event (#302)
Fixes #283
2020-01-25 22:44:06 -08:00
Issac Gerges 4dbc74fccb fix: Make example warning more descriptive by adding path, remove repeated text. Fixes #262. 2020-01-25 22:02:57 -08:00
Issac Gerges f71b1bc8d4 fix: Add original-fs to ignored modules. Fixes #231 2020-01-25 11:36:25 -08:00
Issac Gerges 00c2e1f9a6 style: fixup jsdoc for BisectDialog 2020-01-25 10:28:43 -08:00
Issac Gerges 50f69613dd feat: Add bisect complete dialog. Fixes #299. 2020-01-25 10:28:35 -08:00
Felix Rieseberg 4a23b53b1b 0.12.0 2020-01-15 12:38:05 -08:00
Felix Rieseberg dcd21935a9 Merge pull request #293 from electron/fix-loading-preload
fix: Create initial backup for preload editor
2020-01-15 12:36:43 -08:00
Felix Rieseberg ff37d5cfb9 test: Add tests for new code 2020-01-15 12:10:52 -08:00
Felix Rieseberg bb8053d86d fix: Create a backup for the preload editor 2020-01-15 12:09:59 -08:00
Mark Lee 5efaa54255 feat: publish preload scripts to GitHub gists (#292)
* fix: update welcome tour to mention preload scripts
* chore: update doc comments
2020-01-14 13:41:57 -08:00
Felix Rieseberg b38507230d Merge pull request #291 from electron/update-deps
chore: Update various dependencies
2020-01-14 13:30:30 -08:00
Felix Rieseberg 7451083c70 test: Fix one more test case 2020-01-14 12:16:48 -08:00
Felix Rieseberg be46780674 build: Update tslint rules 2020-01-13 10:55:01 -08:00
Felix Rieseberg 99b5d067df build: Update stylelint 2020-01-13 10:54:25 -08:00
Felix Rieseberg f34eb225ea feat: Update fallback releases 2020-01-13 10:52:49 -08:00
Felix Rieseberg 18cc36a837 feat: Update blueprint 2020-01-13 10:52:23 -08:00
Felix Rieseberg da403e3cbd feat: Update various dependencies 2020-01-13 10:51:04 -08:00
Felix Rieseberg b4431cf426 Merge pull request #285 from electron/direct-fetch-builtins
refactor: directly pull Node.js builtins
2020-01-13 10:46:01 -08:00
Felix Rieseberg e714269b9b Merge pull request #286 from electron/fix-jest-transform
test: avoid transforming JSON files
2020-01-13 10:44:57 -08:00
Felix Rieseberg b9518a63f8 Merge pull request #287 from Loseshape0/patch-1
Update Years
2020-01-13 10:44:28 -08:00
Felix Rieseberg 0627801bb0 Merge pull request #288 from electron/allow-execution-flags
feat: allow users to specify cli flags
2020-01-13 10:43:57 -08:00
Shelley Vohr 3adeece64d spec: fix small test details 2020-01-06 10:52:07 -05:00
Shelley Vohr aed45078c5 spec: update execution settings snapshot 2020-01-06 10:39:10 -05:00
Shelley Vohr 55bfb0241b add handleExecutionFlagChange spec 2020-01-04 21:13:52 -05:00
Shelley Vohr 7e90679706 feat: allow users to specify cli flags 2020-01-04 10:46:59 -05:00
Loseshape0 4897bdbf84 Update Years 2020-01-01 17:41:48 -10:00
Mark Lee 7a02f8cacd test: avoid transforming JSON files 2019-12-27 22:49:16 -08:00
Shelley Vohr a65d594fbc refactor: directly pull Node.js builtins 2019-12-19 21:17:38 -08: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
Felix Rieseberg d039c5dbf5 Merge pull request #155 from electron/bump-version-042
chore: Bump version to 0.4.2
2019-01-08 12:31:30 -08:00
Felix Rieseberg c155fff8b0 chore: Bump version to 0.4.2 2019-01-08 11:40:45 -08:00
Felix Rieseberg 11202a8d6b Merge pull request #152 from electron/100-coverage
test: Full unit test coverage
2019-01-08 11:38:52 -08:00
Felix Rieseberg 475127f5b1 fix: Travis coverage report 2019-01-08 11:17:13 -08:00
Felix Rieseberg feee5be2f8 test: Improve coverage, fix report 2019-01-08 11:11:56 -08:00
Felix Rieseberg 678c7f1578 chore: Fix coverage report 2019-01-08 10:54:59 -08:00
Felix Rieseberg d2fcb8071b test: Correctly report error 2019-01-07 23:18:57 -08:00
Felix Rieseberg 0ab35dc867 test: Update snapshot 2019-01-07 23:16:07 -08:00
Felix Rieseberg 377d221191 chore: Final cleanup 2019-01-07 23:09:42 -08:00
Felix Rieseberg cbdab9d662 test: Cover all of exec 2019-01-07 23:09:42 -08:00
Felix Rieseberg 6875ab392e test: Cover all of versions 2019-01-07 23:09:42 -08:00
Felix Rieseberg 58ec0f015c test: Cover all of file-manager 2019-01-07 23:09:42 -08:00
Felix Rieseberg 2aef0e776e test: Cover all of app 2019-01-07 23:09:42 -08:00
Felix Rieseberg b2a7d6b2a4 test: Cover all of tour 2019-01-07 23:09:42 -08:00
Felix Rieseberg 303d6bc79a test: Cover all of tour-welcome 2019-01-07 23:09:42 -08:00
Felix Rieseberg d028e7c915 test: Cover all of settings-general-appearance 2019-01-07 23:09:42 -08:00
Felix Rieseberg f04631b6c1 test: Linting, cleanup 2019-01-07 23:09:42 -08:00
Felix Rieseberg c1bb524b59 test: Cover all of settings-electron 2019-01-07 23:09:42 -08:00
Felix Rieseberg 4a38fda293 test: Cover all of editors 2019-01-07 23:09:42 -08:00
Felix Rieseberg b98ad2b572 test: Cover all of dialog-token 2019-01-07 23:09:42 -08:00
Felix Rieseberg 6118b94b09 test: Cover all of add-version 2019-01-07 23:09:42 -08:00
Felix Rieseberg d0c1ee1a82 test: Cover all of publish-button 2019-01-07 23:09:42 -08:00
Felix Rieseberg 525715a911 test: Cover all of address-bar 2019-01-07 23:09:42 -08:00
Felix Rieseberg afbc0c9e6c test: Cover squirrel 2019-01-07 23:09:42 -08:00
Felix Rieseberg c1a9cdf88b chore: Add linter 2019-01-07 23:09:42 -08:00
Felix Rieseberg d281e61b8a test: Cover shared-constants (lol) 2019-01-07 23:09:42 -08:00
Felix Rieseberg 2fbbc10497 test: Cover update 2019-01-07 23:09:42 -08:00
Felix Rieseberg e8226ca97d test: Cover all of protocol 2019-01-07 23:09:42 -08:00
Felix Rieseberg 08f2dbe937 test: Cover all of menu 2019-01-07 23:09:42 -08:00
Felix Rieseberg 68813edb51 test: Cover all of main 2019-01-07 23:09:42 -08:00
Felix Rieseberg a32261414b test: Cover all of ipcMainManager 2019-01-07 23:09:42 -08:00
Felix Rieseberg 42f67e7b21 test: Cover all of first-run 2019-01-07 23:09:41 -08:00
Felix Rieseberg 7d9b0ed79d fix: Use localecompare for invalid versions 2019-01-07 23:09:41 -08:00
Felix Rieseberg 47f1b5c4c3 test: Cover all of files 2019-01-07 23:09:41 -08:00
Felix Rieseberg 432322694d test: Correctly use "it" 2019-01-07 23:09:41 -08:00
Felix Rieseberg 889e76cbae test: Cover all of output 2019-01-07 23:09:41 -08:00
Felix Rieseberg 4a689e5c06 test: Cover all of settings-credtis 2019-01-07 23:09:41 -08:00
Felix Rieseberg 3113f324cc test: Cover all of npm 2019-01-07 23:09:41 -08:00
Felix Rieseberg 64addb1fb0 test: Cover all of state 2019-01-07 23:09:41 -08:00
Felix Rieseberg e6facf60c1 test: Cover all of runner & binary 2019-01-07 23:09:41 -08:00
Felix Rieseberg 00cb5cf6c6 Merge pull request #151 from electron/different-constants
fix: Don't require all of electron
2019-01-07 00:39:19 +08:00
Felix Rieseberg 3e8dbebe42 Merge pull request #150 from electron/update-dependencies
feat: Update Electron, Monaco, React, and others
2019-01-07 00:30:24 +08:00
Felix Rieseberg dcdef4aa4e fix: One more incorrect import 2019-01-07 00:25:25 +08:00
Felix Rieseberg 590389abdc fix: Update tests 2019-01-07 00:14:37 +08:00
Felix Rieseberg 18a8f0c142 fix: Deal with incorrect types 2019-01-07 00:10:44 +08:00
Felix Rieseberg a8e8ab6f04 fix: Don't require all of electron 2019-01-07 00:05:17 +08:00
Felix Rieseberg 45ed35467a feat: Update react-mosaic-component 2019-01-06 23:47:36 +08:00
Felix Rieseberg eb49cb296a feat: Update to Electron 3.0.14 2019-01-06 23:39:48 +08:00
Felix Rieseberg c2344d50d2 feat: Update dependencies 2019-01-06 23:33:32 +08:00
Felix Rieseberg 7786e113d9 Merge pull request #148 from electron/fix-defaultversion
fix: Ensure correct version selection & display
2019-01-06 22:58:05 +08:00
Felix Rieseberg f8b51870d8 Merge pull request #146 from electron/use-classnames
refactor: Use classnames module
2019-01-06 21:51:46 +08:00
Felix Rieseberg 601c28dcf8 fix: Handle EEXIST in unzipper 2019-01-06 21:47:25 +08:00
Felix Rieseberg 49379af856 feat: Display "checking status" in run button 2019-01-06 21:47:11 +08:00
Felix Rieseberg 8347f79756 fix: Correctly choose defaultVersion 2019-01-06 21:32:21 +08:00
Felix Rieseberg c102128d5a infra: Use classnames module 2019-01-06 21:01:42 +08:00
Felix Rieseberg fcff053136 Merge pull request #145 from electron/coverage-report
test: Add coverage reports
2019-01-06 20:27:06 +08:00
Felix Rieseberg c3641cfa04 test: Add coverage reports 2019-01-06 20:09:21 +08:00
Felix Rieseberg 6eba3cbdcc Merge pull request #143 from electron/more-tests-2
test: Write even more tests
2019-01-06 19:57:47 +08:00
Felix Rieseberg 6393c59d5c test: One more app spec 2019-01-04 23:56:53 +08:00
Felix Rieseberg 543e86bc91 chore: Fix linter issue 2019-01-04 20:38:34 +08:00
Felix Rieseberg ea5fdd426c test: Add an app setup() spec 2019-01-04 20:33:45 +08:00
Felix Rieseberg fecc499cc5 test: Add an exec test case 2019-01-04 20:13:07 +08:00
Felix Rieseberg 8d0a1524b9 test: Remove impossible case in focused-editor 2019-01-04 20:12:58 +08:00
Felix Rieseberg 918e91965a test: Add a version spec 2019-01-04 19:59:38 +08:00
Felix Rieseberg 15bab9ff86 chore: Fix linter issues 2019-01-04 19:49:37 +08:00
Felix Rieseberg 7d879b59cf test: Add runner specs 2019-01-04 19:42:33 +08:00
Felix Rieseberg 72f7e058d6 test: Add some state tests 2019-01-04 19:26:07 +08:00
Felix Rieseberg a65e044b5c test: Add some file manager tests 2019-01-04 17:13:41 +08:00
Felix Rieseberg a4c4590816 test: Remove obselete snapshot 2019-01-04 16:58:14 +08:00
Felix Rieseberg 6fbb816be3 test: Add MockState 2019-01-04 16:56:32 +08:00
Felix Rieseberg ae0ba026f2 test: More setting tests 2019-01-04 16:56:27 +08:00
Felix Rieseberg cc2a44d10a test: More app tests 2019-01-04 16:56:03 +08:00
Felix Rieseberg 3e6eb16974 Merge pull request #142 from electron/more-tests
test: More test, more minor refactoring
2019-01-02 22:47:52 +08:00
Felix Rieseberg 4212284fc4 test: Update Electron mock 2019-01-02 21:54:46 +08:00
Felix Rieseberg 8b35ff8f95 test: Add octokit spec 2019-01-02 21:54:39 +08:00
Felix Rieseberg 4d00ee9fd4 test: Add files spec 2019-01-02 21:54:27 +08:00
Felix Rieseberg 590db5304d test: Cover sorted map better 2019-01-02 21:54:18 +08:00
Felix Rieseberg e519ce2f13 test: Add highlight spec 2019-01-02 21:54:09 +08:00
Felix Rieseberg 8221920fab test: Add main spec 2019-01-02 21:53:48 +08:00
Felix Rieseberg 5de7ed7d9d test: Move out first-run, add tests 2019-01-02 21:53:36 +08:00
Felix Rieseberg a1586e864a Merge pull request #141 from electron/tests-and-security
test: Increase test coverage, make some code safer
2019-01-02 09:31:36 +08:00
Felix Rieseberg 60745ff8bb test: Add fancy-import test 2018-12-30 19:09:28 +01:00
Felix Rieseberg 4ac20aa5c2 test: Add highlight-text test 2018-12-30 19:09:20 +01:00
Felix Rieseberg 48faec9741 test: Add tests for firstRun, use outputFile 2018-12-30 18:51:08 +01:00
Felix Rieseberg c0f4e95a67 test: Add some version tests 2018-12-30 18:39:39 +01:00
Felix Rieseberg d5c8c8bd96 test: Add some editors tests 2018-12-30 18:18:39 +01:00
Felix Rieseberg 80ff396879 test: Add dialog-warning tests 2018-12-30 17:34:35 +01:00
Felix Rieseberg bf42abc88d test: Update dialog-token tests 2018-12-30 17:11:49 +01:00
Felix Rieseberg eaa064ea8d test: Update dialog-add-version tests 2018-12-30 17:01:57 +01:00
Felix Rieseberg e804436669 test: Update commands-runner tests 2018-12-30 16:58:30 +01:00
Felix Rieseberg 08d07b34a4 chore: Safer commands-runner 2018-12-30 16:53:42 +01:00
Felix Rieseberg 74ba7db13c Merge pull request #140 from electron/fix-cert-file
ci: Properly find Windows codesign certificate
2018-12-24 18:06:42 +01:00
Felix Rieseberg 0fced3aefd ci: Resolve path correctly 2018-12-24 17:56:41 +01:00
Felix Rieseberg 13c9217f9f ci: Fix variable test 2018-12-24 17:52:31 +01:00
Felix Rieseberg 346742b242 ci: Properly find Windows codesign certificate 2018-12-24 17:21:15 +01:00
Felix Rieseberg aefe273516 Merge pull request #139 from electron/bump-041
chore: Bump version number
2018-12-24 16:20:59 +01:00
Felix Rieseberg 337d19c33a chore: Bump version number 2018-12-24 16:15:58 +01:00
Felix Rieseberg ebc6f91dea Merge pull request #138 from electron/fix-tour
fix: The Tour
2018-12-24 16:09:50 +01:00
Felix Rieseberg 25446aa9e4 chore: Update snapshots 2018-12-24 15:27:29 +01:00
Felix Rieseberg f2859c053b chore: Goodbye, dialog component 2018-12-24 14:39:56 +01:00
Felix Rieseberg 49cd192817 feat: Modernize and improve tour 2018-12-24 14:26:09 +01:00
Felix Rieseberg 9bdd09c6b5 fix: Tour selectors 2018-12-24 13:48:13 +01:00
Felix Rieseberg c3d36b29b0 feat: Modernize the tour buttons 2018-12-24 13:44:15 +01:00
Felix Rieseberg 1b69a2ff52 Merge pull request #137 from electron/ci-publish-win
ci: Allow AppVeyor to codesign
2018-12-24 12:19:18 +00:00
Felix Rieseberg 311497d080 ci: AppVeyor: Use Node 10 2018-12-24 13:01:13 +01:00
Felix Rieseberg 55b7739d36 ci: AppVeyor: Upload build artifacts, build ia32 2018-12-24 13:00:28 +01:00
Felix Rieseberg 0fda97eb2f ci: AppVeyor: Print build artifacts 2018-12-24 12:52:45 +01:00
Felix Rieseberg 879815fc02 ci: Allow AppVeyor to codesign 2018-12-24 12:39:33 +01:00
Felix Rieseberg c469c3e856 Merge pull request #136 from electron/fix-windows-tests
test: Fix Windows tests
2018-12-24 08:29:31 +00:00
Felix Rieseberg 1b6750e0c5 Merge pull request #135 from electron/ci-publish
ci: Allow Travis to create signed macOS builds
2018-12-24 08:23:41 +00:00
Felix Rieseberg f14bd5c041 test: Fix Windows tests 2018-12-24 09:22:15 +01:00
Felix Rieseberg 76115a3e9c fix: Little linter error 2018-12-24 09:13:43 +01:00
Felix Rieseberg 65f86e0177 Merge branch 'ci-publish' of https://github.com/electron/fiddle into ci-publish 2018-12-24 09:10:36 +01:00
Felix Rieseberg 18cacb1532 chore: Silence the import 2018-12-24 09:10:27 +01:00
Mark Leeandfelixrieseberg 6a924ac88b Update forge.config.js
Co-Authored-By: felixrieseberg <felix@felixrieseberg.com>
2018-12-24 07:56:55 +00:00
Felix Rieseberg d125af6775 chore: Update the CI config 2018-12-24 08:51:15 +01:00
Felix Rieseberg 1a9a0579a5 chore: Add intermediate macOS certificates 2018-12-24 07:21:30 +01:00
Felix Rieseberg acee131288 chore: Install publisher-github 2018-12-24 07:20:45 +01:00
Felix Rieseberg 66536a5260 Merge pull request #134 from electron/fix-data-dir
feat: Prune appData directories after runs
2018-12-24 05:27:42 +00:00
Shelley Vohr 7941a83109 chore: fix 8 low severity vulns 2018-12-23 10:26:57 -07:00
Felix Rieseberg b312b80b69 fix: Throw error in contributors.js 2018-12-23 18:26:22 +01:00
Shelley Vohr b3c3b73b7d feat: prompt users to move to application folder (#132) 2018-12-23 10:25:37 -07:00
Shelley Vohr 0a8e9cde1b chore: add codebytere to contributors 2018-12-23 10:21:55 -07:00
Felix Rieseberg ca3f48697d chore: Add some tests 2018-12-23 17:57:51 +01:00
Felix Rieseberg 067e5cd5e2 feat: Delete app data dir after run 2018-12-23 17:57:51 +01:00
Felix Rieseberg 7e16c8c1f2 chore: Move runner logic out of component 2018-12-23 17:57:51 +01:00
Felix Rieseberg e932aad47b Merge pull request #131 from electron/electron-311
feat: Update to Electron 3.0.11
2018-12-20 09:45:49 -08:00
Felix Rieseberg 2eb8bb3cf5 infra: Update to Electron 3.0.11 2018-12-20 09:38:04 -08:00
386 changed files with 28121 additions and 21732 deletions
+210
View File
@@ -0,0 +1,210 @@
step-restore-cache: &step-restore-cache
restore_cache:
keys:
- v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
- v1-modules-{{ arch }}
step-install: &step-install
run: npx yarn --frozen-lockfile && rm -rf node_modules/electron/dist
step-save-cache: &step-save-cache
save_cache:
paths:
- node_modules
key: v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
step-install-electron: &step-install-electron
run: cd node_modules/electron && node install.js
steps-test: &steps-test
steps:
- checkout
- *step-restore-cache
- *step-install
- *step-save-cache
- *step-install-electron
- run: yarn lint
- run: yarn test --maxWorkers=4
steps-build: &steps-build
steps:
- checkout
- *step-restore-cache
- *step-install
- *step-save-cache
- *step-install-electron
- when:
condition: <<parameters.on_linux>>
steps:
- run:
name: Install Linux Dependencies
command: sudo apt install -y rpm fakeroot
- when:
condition: <<parameters.on_mac>>
steps:
- run:
name: Import macOS Certificate
command: .circleci/import-macos-cert.sh
- when:
condition: <<parameters.on_win>>
steps:
- run:
name: Import Windows Certificate
command: |
if (Test-Path Env:\WINDOWS_CERTIFICATE_P12) {
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$filename = "$workingDirectory\cert.p12"
$bytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_P12)
[IO.File]::WriteAllBytes($filename, $bytes)
}
shell: powershell.exe
- run:
name: Publish dry run
command: yarn run publish --dry-run --arch=$TARGET_ARCH
- store_artifacts:
path: out/make
destination: dist
- persist_to_workspace:
root: .
paths:
- out/make
- out/publish-dry-run
version: 2.1
orbs:
win: circleci/windows@1.0.0
jobs:
test-linux:
docker:
- image: circleci/node:10
<<: *steps-test
build-linux:
docker:
- image: circleci/node:10-buster
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: true
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: false
<<: *steps-build
test-mac:
macos:
xcode: "10.2.0"
<<: *steps-test
build-mac:
macos:
xcode: "10.2.0"
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: true
on_win:
type: boolean
default: false
<<: *steps-build
test-windows:
executor:
name: win/vs2019
shell: bash.exe
<<: *steps-test
build-windows-ia32:
executor:
name: win/vs2019
shell: bash.exe
environment:
TARGET_ARCH: ia32
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: true
<<: *steps-build
build-windows-x64:
executor:
name: win/vs2019
shell: bash.exe
environment:
TARGET_ARCH: x64
parameters:
on_linux:
type: boolean
default: false
on_mac:
type: boolean
default: false
on_win:
type: boolean
default: true
<<: *steps-build
finalize:
docker:
- image: circleci/node:10
steps:
- checkout
- *step-restore-cache
- *step-install
- attach_workspace:
at: .
- store_artifacts:
path: out/make
destination: dist
release:
docker:
- image: circleci/node:10
steps:
- checkout
- *step-restore-cache
- *step-install
- attach_workspace:
at: .
- run: yarn publish --from-dry-run
workflows:
version: 2
build:
jobs:
- test-linux
- test-mac
- test-windows
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
- finalize:
requires:
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
- release:
requires:
- build-linux
- build-mac
- build-windows-ia32
- build-windows-x64
filters:
branches:
ignore: /.*/
tags:
only: /v.*/
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
set -e
if [[ ! -z $MACOS_CERT_P12 ]]
then
export CERTIFICATE_P12=cert.p12;
echo $MACOS_CERT_P12 | base64 --decode > $CERTIFICATE_P12;
export KEYCHAIN=build.keychain;
# Create the keychain with a password
security create-keychain -p travis $KEYCHAIN;
# Make the custom keychain default, so xcodebuild will use it for signing
security default-keychain -s $KEYCHAIN;
# Unlock the keychain
security unlock-keychain -p travis $KEYCHAIN;
# Add certificates to keychain and allow codesign to access them
# Apple Worldwide Developer Relations Certification Authority
security import ./tools/certs/apple.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer Authentication Certification Authority
security import ./tools/certs/dac.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer ID Felix
security import $CERTIFICATE_P12 -k $KEYCHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign 2>&1 >/dev/null;
rm $CERTIFICATE_P12;
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEYCHAIN
# Echo the identity
security find-identity -v -p codesigning
fi
+31
View File
@@ -0,0 +1,31 @@
const { Octokit } = require('@octokit/action')
const octokit = new Octokit()
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
async function main () {
const prs = await octokit.pulls.list({
owner,
repo,
state: 'open',
head: `${owner}:update-releases`
})
if (prs.data.length === 0) {
const pr = await octokit.pulls.create({
owner,
repo,
title: 'build: update Electron releases JSON',
base: 'master',
head: 'update-releases',
body: 'Auto-update from GitHub Actions.',
maintainer_can_modify: true
})
console.log('Pull request created:', pr.html_url)
} else {
console.log('Pull request updated:', prs.data[0].html_url)
}
}
main()
+51
View File
@@ -0,0 +1,51 @@
name: Auto-update Releases JSON file
on:
schedule:
- cron: '0 0 * * 1'
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fetch git branches
run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Get npm cache directory
id: npm-cache
run: |
echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v1
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- name: Switch to release update branch
run: |
if git branch --remotes | grep -q origin/update-releases; then
git checkout update-releases
else
git checkout -b update-releases
fi
- name: Update Releases JSON
run: npm run electron-releases
- name: Commit Changes to Releases JSON
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "machine github.com login $GITHUB_ACTOR password $GITHUB_TOKEN" > ~/.netrc
chmod 600 ~/.netrc
git add static/releases.json tests/fixtures/releases-metadata.json
if test -n "$(git status -s)"; then
git config user.name "$GITHUB_ACTOR"
git config user.email "electron-bot@users.noreply.github.com"
git diff --cached
git commit -m "build: update Electron releases JSON"
git push origin update-releases
node --unhandled-rejections=strict .github/actions/create_releases_pr.js
else
echo No update needed
fi
+3 -1
View File
@@ -21,7 +21,6 @@
/out/
node_modules/
SHASUMS256.txt
**/yarn.lock
compile_commands.json
.envrc
.cache
@@ -31,6 +30,9 @@ yarn-error.log
# Coverage
coverage
# Test report
report.json
# npm package
/npm/dist
/npm/path.txt
-33
View File
@@ -1,33 +0,0 @@
language: node_js
node_js: "8"
os:
- linux
- osx
dist: trusty
osx_image: xcode8.3
sudo: false
cache:
directories:
- node_modules
- $HOME/.cache/electron
addons:
apt:
packages:
- fakeroot
- rpm
branches:
only:
- master
- /^v\d+\.\d+\.\d+/
install:
- npm install
script:
- npm run lint
- npm run test -- --runInBand
- if test -z "$TRAVIS_TAG"; then npm run make; fi
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
+74
View File
@@ -0,0 +1,74 @@
# Contributing to Electron Fiddle
Electron Fiddle is a community-driven project, overseen by the [Electron Ecosystem Working
Group](https://github.com/electron/governance/tree/master/wg-ecosystem#readme). As such, we welcome
and encourage all sorts of contributions. They include, but are not limited to:
- Constructive feedback
- Bug reports / technical issues
- Documentation changes
- Feature requests
- [Pull requests](#filing-pull-requests)
We strongly suggest that before filing an issue, you search through the existing issues to see
if it has already been filed by someone else.
This project is a part of the Electron ecosystem. As such, all contributions to this project follow
[Electron's code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md)
where appropriate.
## Contribution Suggestions
We use the label [`good first issue`](https://github.com/electron/fiddle/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the issue tracker to denote fairly-well-scoped-out bugs or feature requests that the community can pick up and work on. If any of those labeled issues do not have enough information, please feel free to ask constructive questions. (This applies to any open issue.)
## Filing Pull Requests
Here are some things to keep in mind as you file pull requests to fix bugs, add new features, etc.:
* If you're unfamiliar with forking, branching, and pull requests, please see [GitHub's extensive
documentation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests).
* Travis CI and AppVeyor are used to make sure that any new or changed code meets the project's
style guidelines, and that the project's testsuite passes for each new commit.
* Unless it's impractical, please write tests for your changes. This will help us so that we can
spot regressions much easier.
* When creating commit messages and pull request titles, please adhere to the [conventional
commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
* Please **do not** bump the version number in your pull requests, the maintainers will do that.
Feel free to indicate whether the changes require a major, minor, or patch version bump, as
prescribed by the [semantic versioning specification](http://semver.org/).
### Running Fiddle From Source
1. Clone the Electron Fiddle repository locally.
```sh
git clone https://github.com/electron/fiddle.git
```
2. Change directory to where Fiddle has been cloned.
```sh
cd fiddle
```
3. Install dependencies.
```sh
npm install
```
4. Start Fiddle and Explore!
```sh
npm start
```
### Running Tests
```sh
npm test
```
## Release Process
<!-- TODO @felix add your release process here 😁 -->
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2013-2018 GitHub Inc.
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+10 -6
View File
@@ -1,12 +1,12 @@
# <img src="https://user-images.githubusercontent.com/378023/49785546-4b7f7000-fd64-11e8-8033-a52c73a07fbf.png" width="60px" align="center" alt="Electron Fiddle icon"> Electron Fiddle
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle)
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle) [![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](https://coveralls.io/github/electron/fiddle?branch=master)
Electron Fiddle lets you create and play with small Electron experiments. It
greets you with a quick-start template after opening change a few things,
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
@@ -56,6 +56,10 @@ fiddle has grown up, export it as a project with or without
[electron-forge][electron-forge]. Then, use your favorite editor and take on
the world!
## Contributing
Electron Fiddle is a community-driven project that welcomes all sorts of contributions. Please check out our [Contributing Guide](https://github.com/electron/fiddle/blob/master/CONTRIBUTING.md) for more details.
## License
[MIT, please see the LICENSE file for full details](https://github.com/electron/fiddle/blob/master/LICENSE.md).
-20
View File
@@ -1,20 +0,0 @@
environment:
matrix:
- nodejs_version: "8"
init:
- git config --global core.symlinks true
install:
- ps: Install-Product node $env:nodejs_version x64
- node --version
- npm ci
cache:
- '%APPDATA%\npm-cache -> appveyor.yml'
test_script:
- npm run lint
- npm run test
build: off
+65 -13
View File
@@ -1,12 +1,13 @@
/* tslint:disable */
const path = require('path')
const fs = require('fs')
const packageJson = require('./package.json')
const { version } = packageJson;
const iconDir = path.resolve(__dirname, 'assets', 'icons');
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
module.exports = {
const config = {
hooks: {
generateAssets: require('./tools/generateAssets')
},
@@ -15,12 +16,11 @@ module.exports = {
executableName: 'electron-fiddle',
asar: true,
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
// TODO: FIXME?
// ignore: [
// /^\/\.vscode\//,
// /^\/tools\//
// ],
appBundleId: 'com.electron.fiddle',
usageDescription: {
Camera: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
Microphone: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone'
},
appCategoryType: 'public.app-category.developer-tools',
protocols: [{
name: 'Electron Fiddle Launch Protocol',
@@ -31,14 +31,27 @@ 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: [
{
name: '@electron-forge/maker-squirrel',
platforms: ['win32'],
config: arch => {
config: (arch) => {
const certificateFile = process.env.CI
? path.join(__dirname, 'cert.p12')
: process.env.WINDOWS_CERTIFICATE_FILE;
if (!certificateFile || !fs.existsSync(certificateFile)) {
console.warn(`Warning: Could not find certificate file at ${certificateFile}`)
}
return {
name: 'electron-fiddle',
authors: 'Electron Community',
@@ -49,8 +62,8 @@ module.exports = {
remoteReleases: '',
setupExe: `electron-fiddle-${version}-${arch}-setup.exe`,
setupIcon: path.resolve(iconDir, 'fiddle.ico'),
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
certificateFile
}
}
},
@@ -80,8 +93,47 @@ module.exports = {
owner: 'electron',
name: 'fiddle'
},
prerelease: true
draft: true,
prerelease: false
}
}
]
}
function notarizeMaybe() {
if (process.platform !== 'darwin') {
return;
}
if (process.env.npm_lifecycle_event !== 'publish') {
console.log(`Not in "publish" mode, skipping notarization`);
return;
}
if (!process.env.CI) {
console.log(`Not in CI, skipping notarization`);
return;
}
if (!process.env.TRAVIS_TAG && !process.env.NOTARIZE_WITHOUT_TAG) {
console.log(`Not a tag, not notarizing`);
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
+12 -5
View File
@@ -1,13 +1,18 @@
{
"transform": {
".(js|jsx|ts|tsx)": "ts-jest"
"\\.(js|jsx|ts|tsx)$": "ts-jest"
},
"testURL": "http://localhost",
"testRegex": "(-spec)\\.(ts|tsx)$",
"bail": true,
"resetMocks": true,
"bail": true,
"resetModules": true,
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.js"
],
"moduleFileExtensions": [
"js",
"jsx",
@@ -20,11 +25,13 @@
],
"coverageReporters": [
"json",
"html"
"html",
"lcov"
],
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!**/*.d.ts"
"!**/*.d.ts",
"!**/*constants.ts"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
-16992
View File
File diff suppressed because it is too large Load Diff
+74 -66
View File
@@ -1,27 +1,30 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.4.0",
"version": "0.15.1",
"description": "The easiest way to get started with Electron",
"repository": "https://github.com/electron/fiddle",
"main": "./dist/src/main/main",
"scripts": {
"contributors": "node ./tools/contributors.js",
"less": "node ./tools/lessc.js",
"lint:style": "stylelint ./src/less/*.less --fix",
"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": "npm-run-all lint:*",
"lint:tests": "tslint \"./tests/**/*.ts{,x}\" -c tslint.json --fix",
"lint:templates": "standard \"./static/show-me/**/*.js\"",
"lint": "npm-run-all \"lint:*\"",
"make": "electron-forge make",
"package": "electron-forge package",
"parcel:build": "node ./tools/parcel-build.js",
"parcel:watch": "node ./tools/parcel-watch.js",
"publish": "electron-forge publish",
"start": "rimraf ./dist && electron-forge start",
"test": "jest --config=jest.json --coverage",
"tsc": "node ./tools/tsc.js",
"electron-releases": "node ./tools/fetch-releases.js"
"test": "jest --config=jest.json",
"test:ci": "jest --config=jest.json --coverage --runInBand",
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"test:report": "jest --config=jest.json --json --bail=false --outputFile=report.json | true",
"tsc": "tsc --noEmit -p .",
"electron-releases": "node --unhandled-rejections=strict ./tools/fetch-releases.js"
},
"keywords": [
"Electron",
@@ -34,68 +37,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.24.0",
"@blueprintjs/select": "^3.12.0",
"@electron/get": "^1.9.0",
"@octokit/rest": "^16.43.1",
"@sentry/electron": "^1.2.1",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.1",
"electron-download": "^4.1.1",
"electron-devtools-installer": "^2.2.4",
"electron-squirrel-startup": "^1.0.0",
"extract-zip": "^1.6.7",
"fix-path": "^2.1.0",
"fs-extra": "^7.0.1",
"loader-utils": "^1.1.0",
"mobx": "^5.6.0",
"mobx-react": "^5.4.2",
"monaco-loader": "^0.14.0",
"namor": "^1.1.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-mosaic-component": "^1.1.1",
"semver": "^5.6.0",
"tmp": "0.0.33",
"tslib": "^1.9.3",
"update-electron-app": "^1.3.0"
"fix-path": "^3.0.0",
"fs-extra": "^8.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.20.0",
"monaco-loader": "1.0.0",
"namor": "^2.0.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-mosaic-component": "^3.2.0",
"semver": "^7.1.3",
"tmp": "0.1.0",
"tslib": "^1.11.1",
"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",
"@types/builtin-modules": "^2.0.0",
"@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.4.14",
"@types/react-dom": "^16.0.8",
"@types/semver": "^5.5.0",
"@types/tmp": "0.0.33",
"chokidar": "^2.0.4",
"electron": "3.0.10",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.4",
"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.0",
"node-fetch": "^2.3.0",
"npm-run-all": "^4.1.3",
"parcel-bundler": "^1.10.3",
"react-test-renderer": "^16.4.2",
"rimraf": "^2.6.2",
"standard": "^12.0.1",
"stylelint": "^9.6.0",
"stylelint-config-standard": "^18.2.0",
"ts-jest": "^23.10.5",
"tslint": "^5.11.0",
"tslint-microsoft-contrib": "^5.2.1",
"tslint-react": "^3.6.0",
"typescript": "^3.1.6"
"@babel/core": "^7.8.6",
"@electron-forge/cli": "^6.0.0-beta.52",
"@electron-forge/maker-deb": "^6.0.0-beta.52",
"@electron-forge/maker-rpm": "^6.0.0-beta.52",
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
"@electron-forge/maker-zip": "^6.0.0-beta.52",
"@electron-forge/publisher-github": "^6.0.0-beta.52",
"@octokit/action": "^2.0.0",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.5",
"@types/fs-extra": "^8.1.0",
"@types/jest": "^25.1.3",
"@types/log-symbols": "^3.0.0",
"@types/node": "^13.7.6",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/semver": "^7.1.0",
"@types/tmp": "0.1.0",
"chokidar": "^3.3.1",
"coveralls": "^3.0.9",
"electron": "8.0.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"fetch-mock-jest": "^1.1.0-beta.3",
"jest": "^25.1.0",
"less": "^3.11.1",
"log-symbols": "^3.0.0",
"node-abi": "^2.15.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"react-test-renderer": "^16.13.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^13.2.0",
"stylelint-config-standard": "^20.0.0",
"ts-jest": "^25.2.1",
"tslint": "^6.0.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.2.0",
"typescript": "^3.8.2"
}
}
-14
View File
@@ -1,14 +0,0 @@
import * as electronModule from 'electron';
import * as path from 'path';
const electron = electronModule.remote
? electronModule.remote
: electronModule;
export const USER_DATA_PATH = electron.app.getPath('userData');
export const INDEX_HTML_NAME = 'index.html';
export const MAIN_JS_NAME = 'main.js';
export const RENDERER_JS_NAME = 'renderer.js';
export const PACKAGE_NAME = 'package.json';
export const CONFIG_PATH = path.join(electron.app.getPath('home'), '.electron-fiddle');
+1
View File
@@ -3,6 +3,7 @@ export const html = `<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
<h1>Hello World!</h1>
+2 -14
View File
@@ -3,13 +3,9 @@ const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
const mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
mainWindow.loadURL(url.format({
@@ -20,14 +16,6 @@ function createWindow () {
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// This method will be called when Electron has finished
@@ -47,7 +35,7 @@ app.on('window-all-closed', function () {
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
+11 -17
View File
@@ -1,27 +1,21 @@
export const main = `// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// This method will be called when Electron has finished
@@ -29,10 +23,10 @@ function createWindow () {
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
// Quit when all windows are closed.
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
@@ -41,7 +35,7 @@ app.on('window-all-closed', function () {
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
+48 -8
View File
@@ -4,17 +4,18 @@ export type Files = Map<string, string>;
export type FileTransform = (files: Files) => Promise<Files>;
export enum ElectronVersionState {
export enum VersionState {
ready = 'ready',
downloading = 'downloading',
unzipping = 'unzipping',
unknown = 'unknown'
}
export enum ElectronVersionSource {
export enum VersionSource {
remote = 'remote',
local = 'local'
}
export interface NpmVersion {
export interface Version {
version: string;
name?: string;
localPath?: string;
@@ -24,12 +25,21 @@ export interface EditorValues {
main: string;
renderer: string;
html: string;
preload: string;
css: string;
package?: string;
}
export interface ElectronVersion extends NpmVersion {
state: ElectronVersionState;
source: ElectronVersionSource;
export interface RunnableVersion extends Version {
state: VersionState;
source: VersionSource;
downloadProgress?: number;
}
export interface SetFiddleOptions {
filePath?: string;
templateName?: string;
gistId?: string;
}
export interface OutputEntry {
@@ -43,7 +53,8 @@ export interface OutputOptions {
isNotPre?: boolean;
}
export interface WarningDialogTexts {
export interface GenericDialogOptions {
type: GenericDialogType;
ok?: string;
cancel?: string;
label: string;
@@ -53,6 +64,35 @@ export interface Templates {
[index: string]: string | Templates;
}
export type EditorId = 'main' | 'renderer' | 'html';
export const enum GenericDialogType {
'confirm' = 'confirm',
'warning' = 'warning',
'success' = 'success',
}
// Editors
export const enum EditorId {
'main' = 'main',
'renderer' = 'renderer',
'html' = 'html',
'preload' = 'preload',
'css' = 'css'
}
// 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, EditorId.css];
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'
}
+25 -5
View File
@@ -1,10 +1,12 @@
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',
MONACO_EXECUTE_COMMAND = 'MONACO_EXECUTE_COMMAND',
MONACO_TOGGLE_OPTION = 'MONACO_TOGGLE_OPTION',
FS_NEW_FIDDLE = 'FS_NEW_FIDDLE',
FS_OPEN_FIDDLE = 'FS_OPEN_FIDDLE',
FS_OPEN_TEMPLATE = 'FS_OPEN_TEMPLATE',
@@ -15,24 +17,37 @@ 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_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
TOGGLE_SOFT_WRAP = 'TOGGLE_SOFT_WRAP',
TOGGLE_MINI_MAP = 'TOGGLE_MINI_MAP'
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',
SHOW_INACTIVE = 'SHOW_INACTIVE',
BEFORE_QUIT = 'BEFORE_QUIT',
CONFIRM_QUIT = 'CONFIRM_QUIT',
SET_APPDATA_DIR = 'SET_APPDATA_DIR',
}
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,
IpcEvents.SHOW_INACTIVE,
IpcEvents.CONFIRM_QUIT,
];
export const ipcRendererEvents = [
IpcEvents.OPEN_SETTINGS,
IpcEvents.LOAD_GIST_REQUEST,
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
IpcEvents.FIDDLE_RUN,
IpcEvents.FIDDLE_PACKAGE,
IpcEvents.FIDDLE_MAKE,
IpcEvents.MONACO_EXECUTE_COMMAND,
IpcEvents.MONACO_TOGGLE_OPTION,
IpcEvents.FS_NEW_FIDDLE,
IpcEvents.FS_OPEN_FIDDLE,
IpcEvents.FS_OPEN_TEMPLATE,
@@ -41,6 +56,11 @@ export const ipcRendererEvents = [
IpcEvents.FS_SAVE_FIDDLE_FORGE,
IpcEvents.FS_SAVE_FIDDLE_GIST,
IpcEvents.SHOW_WELCOME_TOUR,
IpcEvents.TOGGLE_SOFT_WRAP,
IpcEvents.TOGGLE_MINI_MAP
IpcEvents.CLEAR_CONSOLE,
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
IpcEvents.BISECT_COMMANDS_TOGGLE,
IpcEvents.BEFORE_QUIT,
IpcEvents.SET_APPDATA_DIR,
];
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;
+37
View File
@@ -2,6 +2,43 @@
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;
}
}
}
// update if list of Editor ID changes
@editor-ids: main, renderer, html, preload;
// takes each editor ID and increase its z-index if the parent Mosiac root
// has focused__id class for that specific id.
each(@editor-ids, {
.focused__@{value}.mosaic .mosaic-window.@{value} {
z-index: 2;
}
});
.mosaic-window {
z-index: 1;
}
@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;
}
+4
View File
@@ -6,3 +6,7 @@
width: 100vw;
z-index: 200;
}
.tour-portal {
z-index: 1000;
}
+5
View File
@@ -0,0 +1,5 @@
.bp3-fill {
.version-chooser {
width: 100%;
}
}
-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);
}
+2 -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,5 @@
@import "components/chrome-mac.less";
@import "components/editors.less";
@import "components/tour.less";
@import "components/commands-version-chooser.less";
@import "components/show-me.less";
@import "components/version-select.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]]);
}
+25 -23
View File
@@ -2,56 +2,58 @@ import { dialog } from 'electron';
import * as fs from 'fs-extra';
import * as path from 'path';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
import { IpcEvents } from '../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { ipcMainManager } from './ipc';
/**
* Ensures that we're listening to file events
*/
export function setupFileListeners() {
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => showSaveDialog());
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => {
showSaveDialog();
});
}
/**
* 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] ]);
}
}
/**
+34
View File
@@ -0,0 +1,34 @@
import { app, dialog } from 'electron';
import { isFirstRun } from '../utils/check-first-run';
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 async function onFirstRunMaybe() {
if (isFirstRun()) {
await promptMoveToApplicationsFolder();
}
}
/**
* Ask the user if the app should be moved to the
* applications folder.
*/
async function promptMoveToApplicationsFolder(): Promise<void> {
if (process.platform !== 'darwin') return;
if (isDevMode() || app.isInApplicationsFolder()) return;
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Move to Applications Folder', 'Do Not Move'],
defaultId: 0,
message: 'Move to Applications Folder?',
});
if (response === 0) {
app.moveToApplicationsFolder();
}
}
+22 -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,27 @@ 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.removeAllListeners(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 +47,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);
}
+69 -20
View File
@@ -1,28 +1,30 @@
import { initSentry } from '../sentry';
initSentry();
import { app } from 'electron';
import { IpcEvents } from '../ipc-events';
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 { ipcMainManager } from './ipc';
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
import { shouldQuit } from './squirrel';
import { setupUpdates } from './update';
import { getOrCreateMainWindow } from './windows';
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit();
}
app.setName('Electron Fiddle');
listenForProtocolHandler();
app.on('ready', async () => {
// If we're packaged, we want to run
// React in production mode.
if (!isDevMode()) {
process.env.NODE_ENV = 'production';
}
/**
* Handle the app's "ready" event. This is essentially
* the method that takes care of booting the application.
*/
export async function onReady() {
await onFirstRunMaybe();
if (!isDevMode()) process.env.NODE_ENV = 'production';
getOrCreateMainWindow();
setupAboutPanel();
const { setupMenu } = await import('./menu');
const { setupFileListeners } = await import('./files');
@@ -32,15 +34,62 @@ app.on('ready', async () => {
setupFileListeners();
setupUpdates();
setupDialogs();
});
setupDevTools();
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
/**
* Handle the "before-quit" event
*
* @export
*/
export function onBeforeQuit() {
ipcMainManager.send(IpcEvents.BEFORE_QUIT);
ipcMainManager.on(IpcEvents.CONFIRM_QUIT, quitAppIfConfirmed);
}
export function quitAppIfConfirmed(_: Electron.IpcMainEvent, quitConfirmed: boolean) {
if (quitConfirmed) {
app.quit();
}
}
/**
* All windows have been closed, quit on anything but
* macOS.
*/
export function onWindowsAllClosed() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
}
app.on('activate', getOrCreateMainWindow);
/**
* The main method - and the first function to run
* when Fiddle is launched.
*
* Exported for testing purposes.
*/
export function main() {
// Handle creating/removing shortcuts on Windows when
// installing/uninstalling.
if (shouldQuit()) {
app.quit();
return;
}
// Set the app's name
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);
}
main();
+20 -14
View File
@@ -105,9 +105,7 @@ function getQuitItems(): Array<MenuItemConstructorOptions> {
{
type: 'separator'
}, {
label: 'Exit',
accelerator: 'Ctrl+Q,',
click: app.quit
role: 'quit'
}
];
}
@@ -121,6 +119,7 @@ function getTasksMenu(): MenuItemConstructorOptions {
const tasksMenu: Array<MenuItemConstructorOptions> = [
{
label: 'Run Fiddle...',
accelerator: 'F5',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
@@ -143,7 +142,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 +158,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 +204,7 @@ function getFileMenu(): MenuItemConstructorOptions {
type: 'separator'
},
{
label: 'Save to Gist',
label: 'Publish to Gist',
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
},
{
@@ -238,7 +237,7 @@ export function setupMenu() {
// Append the "Settings" item
if (
process.platform === 'darwin'
&& label === app.getName()
&& label === app.name
&& isSubmenu(item.submenu)
) {
item.submenu.splice(2, 0, ...getPreferencesItems());
@@ -246,15 +245,22 @@ 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
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
item.submenu = item.submenu
.filter((subItem) => subItem.label !== 'Toggle Developer Tools' && subItem.label !== 'Reload');
item.submenu.push({ type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }); // Add zooming actions
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Soft Wrap',
click: () => ipcMainManager.send(IpcEvents.TOGGLE_SOFT_WRAP),
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Mini Map',
click: () => ipcMainManager.send(IpcEvents.TOGGLE_MINI_MAP),
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',
});
}
+53 -6
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';
@@ -14,32 +15,78 @@ const handlePotentialProtocolLaunch = (url: string) => {
app.once('ready', () => handlePotentialProtocolLaunch(url));
return;
}
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
if (!parsed.pathname) return;
const pathParts = parsed.pathname.split('/');
if (pathParts.length <= 0 || pathParts.length > 2) return;
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [parsed.pathname]);
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
if (!parsed.pathname || !parsed.hostname) return;
const pathParts = parsed.pathname.split('/').slice(1);
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();
app.removeAllListeners('open-url');
app.on('open-url', (_, url) => {
if (url.startsWith(`${PROTOCOL}://`)) {
handlePotentialProtocolLaunch(url);
}
});
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);
}
};
+3
View File
@@ -0,0 +1,3 @@
export function shouldQuit() {
return require('electron-squirrel-startup');
}
+3 -1
View File
@@ -5,7 +5,9 @@ export function setupUpdates() {
// We delay this work by 10s to ensure that the
// app doesn't have to worry about updating during launch
setTimeout(() => {
require('update-electron-app')({
const updateApp = require('update-electron-app');
updateApp({
repo: 'electron/fiddle',
updateInterval: '1 hour',
});
+21 -6
View File
@@ -1,9 +1,11 @@
import { BrowserWindow, shell } from 'electron';
import { app, BrowserWindow, shell } from 'electron';
import { IpcEvents } from '../ipc-events';
import { createContextMenu } from './context-menu';
import { ipcMainManager } from './ipc';
// Keep a global reference of the window objects, if we don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
export let browserWindows: Array<BrowserWindow> = [];
export let browserWindows: Array<BrowserWindow | null> = [];
/**
* Gets default options for the main window
@@ -20,7 +22,8 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
acceptFirstMouse: true,
backgroundColor: '#1d2427',
webPreferences: {
webviewTag: false
webviewTag: false,
nodeIntegration: true
}
};
}
@@ -34,13 +37,14 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
*/
export function createMainWindow(): Electron.BrowserWindow {
console.log(`Creating main window`);
const browserWindow = new BrowserWindow(getMainWindowOptions());
let browserWindow: BrowserWindow | null;
browserWindow = new BrowserWindow(getMainWindowOptions());
browserWindow.loadFile('./dist/static/index.html');
browserWindow.webContents.once('dom-ready', () => {
browserWindow.show();
if (browserWindow) {
browserWindow.show();
createContextMenu(browserWindow);
}
});
@@ -48,6 +52,8 @@ export function createMainWindow(): Electron.BrowserWindow {
browserWindow.on('closed', () => {
browserWindows = browserWindows
.filter((bw) => browserWindow !== bw);
browserWindow = null;
});
browserWindow.webContents.on('new-window', (event, url) => {
@@ -60,6 +66,15 @@ export function createMainWindow(): Electron.BrowserWindow {
shell.openExternal(url);
});
ipcMainManager.on(IpcEvents.SHOW_INACTIVE, () => {
if (browserWindow) {
browserWindow.showInactive();
}
});
const appData = app.getPath('appData');
ipcMainManager.send(IpcEvents.SET_APPDATA_DIR, [appData], browserWindow.webContents);
browserWindows.push(browserWindow);
return browserWindow;
+126 -76
View File
@@ -1,12 +1,30 @@
import { initSentry } from '../sentry';
initSentry();
import { when } from 'mobx';
import * as MonacoType from 'monaco-editor';
import { EditorValues } from '../interfaces';
import { ipcRenderer } from 'electron';
import {
ALL_EDITORS,
EditorId,
EditorValues,
GenericDialogType,
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 { EMPTY_EDITOR_CONTENT } from './constants';
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,
@@ -19,75 +37,122 @@ 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;
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.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
this.state.isGenericDialogShowing = true;
await when(() => !this.state.isGenericDialogShowing);
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (appState.isUnsaved && warn) {
this.state.isWarningDialogShowing = true;
await when(() => !this.state.isWarningDialogShowing);
if (!this.state.warningDialogLastResult) {
if (!this.state.genericDialogLastResult) {
return false;
}
}
const { main, html, renderer } = fiddle.editors;
// display all editors that have content
const visibleEditors = [];
for (const id of Object.keys(editorValues)) {
const content = editorValues[id];
if (html && html.setValue && values.html) {
html.setValue(values.html);
// if the gist content matches the empty file output, don't show it
if (!Object.values(EMPTY_EDITOR_CONTENT).includes(content)) {
visibleEditors.push(id as EditorId);
}
}
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;
appState.isUnsaved = false;
this.setupUnsavedOnChangeListener();
// once loaded, we have a "saved" state
await this.state.setVisibleMosaics(visibleEditors);
await this.setEditorValues(editorValues);
this.state.isUnsaved = false;
return true;
}
/**
* Gets the values on all three editors.
* Sets the contents of all editor panes.
*
* @returns {EditorValues}
* @param {EditorValues} values
*/
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)) {
// The editor does not exist, attempt to set it on the backup.
// If there's a model, we'll do it on the model. Else, we'll
// set the value.
if (backup.model) {
backup.model.setValue(values[name]!);
} else {
backup.value = values[name]!;
}
} else if (editor && editor.setValue) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
}
}
}
}
/**
* Retrieves the contents of all editor panes.
*
* @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() : '',
css: getEditorValue(EditorId.css),
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
values.package = await getPackageJson(appState, values, options);
if (options && options.include !== false) {
values.package = await getPackageJson(this.state, values, options);
}
return values;
@@ -97,48 +162,33 @@ export class App {
* Initial setup call, loading Monaco and kicking off the React
* render process.
*/
public async setup(): Promise<void> {
public async setup(): Promise<void | Element | React.Component> {
this.setupTheme();
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={appState} />
<Dialogs appState={appState} />
<Editors appState={appState} />
<Dialogs appState={this.state} />
<Header appState={this.state} />
<OutputEditorsWrapper appState={this.state} />
</div>
);
render(app, document.getElementById('app'));
const rendered = render(app, document.getElementById('app'));
this.setupResizeListener();
// Todo: A timer here is terrible. Let's fix this
// and ensure we actually do it once Editors have mounted.
setTimeout(() => {
this.setupUnsavedOnChangeListener();
}, 1500);
}
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
/**
* If the editor is changed for the first time, we'll
* set `isUnsaved` to true. That way, the app can warn you
* if you're about to throw things away.
*/
public setupUnsavedOnChangeListener() {
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor = window.ElectronFiddle.editors[key];
const disposable = editor.onDidChangeModelContent(() => {
appState.isUnsaved = true;
disposable.dispose();
});
});
return rendered;
}
/**
@@ -147,14 +197,16 @@ export class App {
* @returns {Promise<void>}
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
const theme = await getTheme(appState.theme);
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
if (tag && theme.css) {
tag.innerHTML = theme.css;
}
if (theme.isDark || (theme.name || '').includes('dark')) {
if (theme.isDark || theme.name.includes('dark')) {
document.body.classList.add('bp3-dark');
} else {
document.body.classList.remove('bp3-dark');
@@ -170,10 +222,8 @@ export class App {
}
}
// tslint:disable-next-line:no-string-literal
if (!process.env.TEST && !process.env.JEST_WORKER_ID) {
window.ElectronFiddle.contentChangeListeners = [];
window.ElectronFiddle.app = new App();
window.ElectronFiddle.app.setup()
.catch((error) => console.error(error));
}
window.ElectronFiddle = window.ElectronFiddle || {};
window.ElectronFiddle.contentChangeListeners =
window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
window.ElectronFiddle.app.setup();
+237 -173
View File
@@ -1,195 +1,259 @@
import * as fsType from 'fs-extra';
import * as path from 'path';
import { USER_DATA_PATH } from '../constants';
import { VersionState } from '../interfaces';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { USER_DATA_PATH } from './constants';
import { removeTypeDefsForVersion } from './fetch-types';
import { AppState } from './state';
/**
* The binary manager takes care of downloading Electron versions
* General setup, called with a version. Is called during construction
* to ensure that we always have or download at least one version.
*
* @export
* @class BinaryManager
* @param {string} iVersion
* @returns {Promise<void>}
*/
export class BinaryManager {
public state: Record<string, 'ready' | 'downloading'> = {};
export async function setupBinary(appState: AppState, iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
/**
* Remove a version from disk. Does not update state. We'll try up to
* 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> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(getDownloadPath(version));
const { state } = appState.versions[version];
if (state === VersionState.downloading || state === VersionState.unzipping) {
console.log(`Binary: Electron ${version} already downloading.`);
return;
}
if (await getIsDownloaded(version)) {
console.log(`Binary: Electron ${version} already downloaded.`);
appState.versions[version].state = VersionState.ready;
return;
}
console.log(`Binary: Electron ${version} not present, downloading`);
appState.versions[version].state = VersionState.downloading;
const zipPath = await download(appState, version);
const extractPath = getDownloadPath(version);
console.log(`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`);
try {
appState.versions[version].state = VersionState.unzipping;
// Ensure the target path is empty
await fs.emptyDir(extractPath);
const electronFiles = await unzip(zipPath, extractPath);
console.log(`Unzipped ${version}`, electronFiles);
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
appState.versions[version].state = VersionState.unknown;
return;
}
appState.versions[version].state = VersionState.ready;
}
/**
* Remove a version from disk. Does not update state. We'll try up to
* four times before giving up if an error occurs.
*
* @param {string} iVersion
* @returns {Promise<void>}
*/
export async function removeBinary(iVersion: string) {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
let isDeleted = false;
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
try {
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;
}
await func();
} 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);
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);
}
}
}
/**
* General setup, called with a version. Is called during construction
* to ensure that we always have or download at least one version.
*
* @param {string} iVersion
* @returns {Promise<void>}
*/
public async setup(iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
const { promisify } = await import('util');
const eDownload = promisify(require('electron-download'));
await fs.mkdirp(this.getDownloadPath(version));
if (this.state[version] === 'downloading') {
console.log(`BinaryManager: Electron ${version} already downloading.`);
return;
}
if (await this.getIsDownloaded(version)) {
console.log(`BinaryManager: Electron ${version} already downloaded.`);
this.state[version] = 'ready';
return;
}
console.log(`BinaryManager: Electron ${version} not present, downloading`);
this.state[version] = 'downloading';
const zipPath = await eDownload({ version });
const extractPath = this.getDownloadPath(version);
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
try {
const electronFiles = await this.unzip(zipPath, extractPath);
console.log(`Unzipped ${version}`, electronFiles);
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
// Todo: Handle this case
}
this.state[version] = 'ready';
}
/**
* Gets the expected path for the binary of a given Electron version
*
* @param {string} version
* @param {string} dir
* @returns {string}
*/
public getElectronBinaryPath(
version: string,
dir: string = this.getDownloadPath(version),
): string {
switch (process.platform) {
case 'darwin':
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
case 'freebsd':
case 'linux':
return path.join(dir, 'electron');
case 'win32':
return path.join(dir, 'electron.exe');
default:
throw new Error(`Electron builds are not available for ${process.platform}`);
}
}
/**
* Returns an array of all versions downloaded to disk
*
* @returns {Promise<Array<string>>}
*/
public async getDownloadedVersions(): Promise<Array<string>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
console.log(`BinaryManager: Checking for downloaded versions`);
try {
const directories = await fs.readdir(downloadPath);
const knownVersions: Array<string> = [];
for (const directory of directories) {
if (await this.getIsDownloaded(directory)) {
knownVersions.push(directory);
}
}
return knownVersions;
} catch (error) {
console.warn(`Could not read known Electron versions`);
return [];
}
}
/**
* Did we already download a given version?
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
public async getIsDownloaded(version: string, dir?: string): Promise<boolean> {
const expectedPath = this.getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(expectedPath);
}
/**
* Gets the expected path for a given Electron version
*
* @param {string} version
* @returns {string}
*/
private getDownloadPath(version: string): string {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actaully use it.
*
* @param {string} zipPath
* @param {string} extractPath
* @returns {Promise<void>}
*/
private unzip(zipPath: string, extractPath: string): Promise<void> {
return new Promise(async (resolve, reject) => {
const extract = (await fancyImport<any>('extract-zip')).default;
};
const binaryCleaner = async () => {
if (await 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(getDownloadPath(version));
process.noAsar = false;
extract(zipPath, { dir: extractPath }, (error: Error) => {
if (error) {
reject(error);
return;
}
isDeleted = true;
}
};
console.log(`BinaryManager: Unpacked!`);
process.noAsar = false;
const typeDefsCleaner = async () => {
await removeTypeDefsForVersion(version);
};
resolve();
});
});
await rerunner(binaryCleaner);
if (isDeleted) {
await rerunner(typeDefsCleaner);
}
}
/* Did we already download a given version?
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
export async function getIsDownloaded(version: string, dir?: string): Promise<boolean> {
const expectedPath = getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(expectedPath);
}
/**
* Gets the expected path for the binary of a given Electron version
*
* @param {string} version
* @param {string} dir
* @returns {string}
*/
export function getElectronBinaryPath(
version: string,
dir: string = getDownloadPath(version),
): string {
switch (process.platform) {
case 'darwin':
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
case 'freebsd':
case 'linux':
return path.join(dir, 'electron');
case 'win32':
return path.join(dir, 'electron.exe');
default:
throw new Error(`Electron builds are not available for ${process.platform}`);
}
}
export function getDownloadingVersions(appState: AppState) {
return Object.entries(appState.versions)
.filter(([_, { state }]) => state === 'downloading')
.map(([version, _]) => version);
}
/**
* Returns an array of all versions downloaded to disk
*
* @returns {Promise<Array<string>>}
*/
export async function getDownloadedVersions(): Promise<Array<string>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
console.log(`Binary: Checking for downloaded versions`);
try {
const directories = await fs.readdir(downloadPath);
const knownVersions: Array<string> = [];
for (const directory of directories) {
if (await getIsDownloaded(directory)) {
knownVersions.push(directory);
}
}
return knownVersions;
} catch (error) {
console.warn(`Could not read known Electron versions`);
return [];
}
}
/**
* Download an Electron version.
*
* @param {AppState} appState
* @param {string} version
* @returns {Promise<string>}
*/
async function download(appState: AppState, version: string): Promise<string> {
const { download: electronDownload } = await import('@electron/get');
const getProgressCallback = (progress: Progress) => {
const roundedProgress = Math.round(progress.percent * 100) / 100;
if (roundedProgress !== appState.versions[version].downloadProgress) {
console.debug(`Binary: Version ${version} download progress: ${progress.percent}`);
appState.versions[version].downloadProgress = roundedProgress;
}
};
if (!appState.versions[version]) {
throw new Error(`Version ${version} does not exist in state, cannot download`);
}
const zipFilePath = await electronDownload(version, {
downloadOptions: {
quiet: true,
getProgressCallback
}
});
return zipFilePath;
}
/**
* Gets the expected path for a given Electron version
*
* @param {string} version
* @returns {string}
*/
function getDownloadPath(version: string): string {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actually use it.
*
* @param {string} zipPath
* @param {string} extractPath
* @returns {Promise<void>}
*/
function unzip(zipPath: string, extractPath: string): Promise<void> {
return new Promise(async (resolve, reject) => {
const extract = (await fancyImport<any>('extract-zip')).default;
process.noAsar = true;
const options = {
dir: extractPath,
};
extract(zipPath, options, (error: Error) => {
if (error) {
reject(error);
return;
}
console.log(`Binary: Unpacked!`);
process.noAsar = false;
resolve();
});
});
}
interface Progress {
percent: number;
transferred: number;
total: number;
}
+58
View File
@@ -0,0 +1,58 @@
import { RunnableVersion } from '../interfaces';
export class Bisector {
public revList: Array<RunnableVersion>;
public minRev: number;
public maxRev: number;
private pivot: number;
constructor(revList: Array<RunnableVersion>) {
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>
);
@@ -1,13 +1,12 @@
import { Button, InputGroup, Intent } from '@blueprintjs/core';
import classnames from 'classnames';
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 '../../constants';
import { classNames } from '../../utils/classnames';
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 });
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,85 @@
import { Button } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { GenericDialogType, VersionState } from '../../../src/interfaces';
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) {
window.ElectronFiddle.app.runner.stop();
const { appState } = this.props;
const response = appState.Bisector!.continue(isGood);
if (Array.isArray(response)) {
this.terminateBisect();
const [minRev, maxRev] = response;
const [minVer, maxVer] = [minRev.version, maxRev.version];
const message = `Check between versions ${minVer} and ${maxVer}.`;
appState.pushOutput(`[BISECT] Complete: ${message}`);
appState.setGenericDialogOptions({
type: GenericDialogType.success,
label: `Bisect complete. ${message}`,
cancel: undefined
});
appState.isGenericDialogShowing = true;
} else {
appState.setVersion(response.version);
}
}
public terminateBisect() {
const { appState } = this.props;
appState.Bisector = undefined;
}
public render() {
const { appState } = this.props;
if (!!appState.Bisector) {
const isDownloading = appState.currentElectronVersion.state === VersionState.downloading;
return (
<>
<Button
icon={'thumbs-up'}
onClick={() => this.continueBisect(true)}
disabled={isDownloading}
/>
<Button
icon={'thumbs-down'}
onClick={() => this.continueBisect(false)}
disabled={isDownloading}
/>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
);
} else {
return (
<Button
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
}
}
}
@@ -0,0 +1,113 @@
import { Button, Menu, MenuDivider, 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}
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
/>
);
}
result.push(
<React.Fragment key={'fragment-reset-layout'}>
<MenuDivider />
<MenuItem
icon='grid-view'
key='reset-layout'
text='Reset Layout'
onClick={appState.resetEditorLayout}
/>
</React.Fragment>
);
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,13 @@
import { Button, ButtonGroup, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
import { Button, ButtonGroup, IToastProps, Menu, MenuItem, Popover, Position, Toaster } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { when } from 'mobx';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
import { EditorValues } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../../shared-constants';
import { getOctokit } from '../../utils/octokit';
import { EMPTY_EDITOR_CONTENT } from '../constants';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -13,8 +15,9 @@ export interface PublishButtonProps {
appState: AppState;
}
export interface PublishButtonState {
isPublishing: boolean;
interface IPublishButtonState {
readonly isUpdating: boolean;
readonly wouldPublish: boolean;
}
/**
@@ -25,19 +28,33 @@ export interface PublishButtonState {
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
*/
@observer
export class PublishButton extends React.Component<PublishButtonProps, PublishButtonState> {
constructor(props: PublishButtonProps) {
export class PublishButton extends React.Component<PublishButtonProps, IPublishButtonState> {
public constructor(props: PublishButtonProps) {
super(props);
this.state = { isPublishing: false };
this.handleClick = this.handleClick.bind(this);
this.publishFiddle = this.publishFiddle.bind(this);
this.publishOrUpdateFiddle = this.publishOrUpdateFiddle.bind(this);
this.setPrivate = this.setPrivate.bind(this);
this.setPublic = this.setPublic.bind(this);
this.state = {
isUpdating: false,
wouldPublish: false,
};
}
private toaster: Toaster;
private refHandlers = {
toaster: (ref: Toaster) => this.toaster = ref,
};
public componentDidMount() {
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
public componentWillUnmount() {
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
/**
* When the user clicks the publish button, we either show the
* authentication dialog or publish right away.
@@ -60,7 +77,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
if (appState.gitHubToken) {
return this.publishFiddle();
return this.publishOrUpdateFiddle();
}
}
@@ -68,60 +85,147 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
* Connect with GitHub, publish the current Fiddle as a gist,
* and update all related properties in the app state.
*/
public async publishFiddle(): Promise<void> {
this.setState({ isPublishing: true });
public async publishOrUpdateFiddle(): Promise<void> {
const { appState } = this.props;
const { wouldPublish } = this.state;
appState.isPublishing = true;
const octo = await getOctokit();
octo.authenticate({
type: 'token',
token: this.props.appState.gitHubToken!
});
const { gitHubPublishAsPublic } = this.props.appState;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic, gistId } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getValues(options);
const values = await window.ElectronFiddle.app.getEditorValues(options);
try {
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description: 'Electron Fiddle Gist',
files: {
[INDEX_HTML_NAME]: {
content: values.html || '<!-- Empty -->',
},
[MAIN_JS_NAME]: {
content: values.main || '// Empty',
},
[RENDERER_JS_NAME]: {
content: values.renderer || '// Empty',
},
},
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
if (gistId && !wouldPublish) {
this.setState({
isUpdating: true
});
const gist = await octo.gists.update({
gist_id: appState.gistId,
files: this.gistFilesList(values) as any,
});
this.props.appState.gistId = gist.data.id;
console.log('Updating: Updating done', { gist });
this.renderToast({ message: 'Successfully updated gist!' });
this.setState({
isUpdating: false
});
} else {
try {
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description: 'Electron Fiddle Gist',
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
});
console.log(`Publish Button: Publishing done`, { gist });
} catch (error) {
console.warn(`Could not publish gist`, { error });
appState.gistId = gist.data.id;
const messageBoxOptions: Electron.MessageBoxOptions = {
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`
};
console.log(`Publish Button: Publishing done`, { gist });
this.renderToast({ message: 'Publishing done successfully!' });
} catch (error) {
console.warn(`Could not publish gist`, { error });
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
const messageBoxOptions: Electron.MessageBoxOptions = {
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
}
this.setState({ isPublishing: false });
appState.isPublishing = false;
}
public setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
/**
* Publish fiddles as private.
*
* @memberof PublishButton
*/
public setPrivate() {
this.setPrivacy(false);
}
/**
* Publish fiddles as public.
*
* @memberof PublishButton
*/
public setPublic() {
this.setPrivacy(true);
}
public render() {
const { gitHubPublishAsPublic } = this.props.appState;
const { isPublishing } = this.state;
const { isPublishing, gistId } = this.props.appState;
const { isUpdating, wouldPublish } = this.state;
const getTextForButton = gistId && !wouldPublish
? 'Update'
: isUpdating
? 'Updating...'
: isPublishing
? 'Publishing...'
: 'Publish';
return (
<>
<fieldset disabled={isPublishing}>
<ButtonGroup className='button-publish'>
{this.renderPrivaryMenu()}
<Button
onClick={this.handleClick}
loading={isPublishing}
icon='upload'
text={getTextForButton}
/>
{this.renderMaybePublishMenu()}
</ButtonGroup>
</fieldset>
<Toaster position={Position.BOTTOM_RIGHT} ref={this.refHandlers.toaster} />
</>
);
}
private renderMaybePublishMenu = () => {
const { gistId } = this.props.appState;
const { wouldPublish } = this.state;
if (!gistId) {
return null;
}
const menu = (
<Menu>
<MenuItem
text='Publish'
active={wouldPublish}
onClick={() => this.setWouldPublish(true)}
/>
<MenuItem
text='Update'
active={!wouldPublish}
onClick={() => this.setWouldPublish(false)}
/>
</Menu>
);
return (
<Popover
content={menu}
position={Position.BOTTOM}
>
<Button
icon='wrench'
/>
</Popover>
);
}
private renderPrivaryMenu = () => {
const { gitHubPublishAsPublic, gistId } = this.props.appState;
if (gistId) {
return null;
}
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
@@ -130,36 +234,60 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
text='Private'
icon='lock'
active={!gitHubPublishAsPublic}
onClick={() => this.setPrivacy(false)}
onClick={this.setPrivate}
/>
<MenuItem
text='Public'
icon='unlock'
active={gitHubPublishAsPublic}
onClick={() => this.setPrivacy(true)}
onClick={this.setPublic}
/>
</Menu>
);
return (
<ButtonGroup>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
disabled={isPublishing}
>
<Button
icon={privacyIcon}
/>
</Popover>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
>
<Button
onClick={this.handleClick}
loading={isPublishing}
disabled={isPublishing}
icon='upload'
text={isPublishing ? 'Publishing...' : 'Publish'}
icon={privacyIcon}
/>
</ButtonGroup>
</Popover>
);
}
private setWouldPublish = (wouldPublish: boolean) => {
this.setState({
wouldPublish,
});
}
private setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
}
private renderToast = (toast: IToastProps) => {
this.toaster.show(toast);
}
private gistFilesList = (values: EditorValues) => {
return {
[INDEX_HTML_NAME]: {
content: values.html || EMPTY_EDITOR_CONTENT.html,
},
[MAIN_JS_NAME]: {
content: values.main || EMPTY_EDITOR_CONTENT.js,
},
[RENDERER_JS_NAME]: {
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
},
[PRELOAD_JS_NAME]: {
content: values.preload || EMPTY_EDITOR_CONTENT.js,
},
[STYLES_CSS_NAME]: {
content: values.css || EMPTY_EDITOR_CONTENT.css,
},
};
}
}
+18 -275
View File
@@ -1,31 +1,17 @@
import { Button, IButtonProps } from '@blueprintjs/core';
import { ChildProcess, spawn } from 'child_process';
import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as path from 'path';
import * as React from 'react';
import { EditorValues, ElectronVersionSource, ElectronVersionState, FileTransform } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { PackageJsonOptions } from '../../utils/get-package';
import { normalizeVersion } from '../../utils/normalize-version';
import { maybePlural } from '../../utils/plural-maybe';
import { ipcRendererManager } from '../ipc';
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from '../npm';
import { VersionState } from '../../interfaces';
import { AppState } from '../state';
export interface RunnerState {
isRunning: boolean;
}
export interface RunnerProps {
appState: AppState;
}
export enum ForgeCommands {
PACKAGE = 'package',
MAKE = 'make'
}
/**
* The runner component is responsible for actually launching the fiddle
* with Electron. It also renders the button that does so.
@@ -35,279 +21,36 @@ export enum ForgeCommands {
*/
@observer
export class Runner extends React.Component<RunnerProps, RunnerState> {
public child: ChildProcess | null = null;
constructor(props: RunnerProps) {
super(props);
this.run = this.run.bind(this);
this.performForgeOperation = this.performForgeOperation.bind(this);
this.stop = this.stop.bind(this);
this.state = { isRunning: false };
this.props.appState.pushOutput('Console ready 🔬');
}
public componentDidMount() {
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
this.performForgeOperation(ForgeCommands.PACKAGE);
});
ipcRendererManager.on(IpcEvents.FIDDLE_MAKE, () => {
this.performForgeOperation(ForgeCommands.MAKE);
});
}
public render() {
const { versions, version } = this.props.appState;
const { isRunning } = this.state;
const { isRunning, currentElectronVersion } = this.props.appState;
if (!versions || !version || !versions[normalizeVersion(version)]) {
return null;
}
const state = currentElectronVersion && currentElectronVersion.state;
const props: IButtonProps = { className: 'button-run', disabled: true };
const state = versions[normalizeVersion(version)].state;
const props: IButtonProps = {};
if (state === ElectronVersionState.downloading) {
if (state === VersionState.downloading) {
props.text = 'Downloading';
props.disabled = true;
props.loading = true;
} else if (state === ElectronVersionState.ready) {
props.icon = <Spinner size={16} value={currentElectronVersion?.downloadProgress} />;
} else if (state === VersionState.unzipping) {
props.text = 'Unzipping';
props.icon = <Spinner size={16} />;
} else if (state === VersionState.ready) {
props.disabled = false;
if (isRunning) {
props.active = true;
props.text = 'Stop';
props.onClick = this.stop;
props.onClick = window.ElectronFiddle.app.runner.stop;
props.icon = 'stop';
} else {
props.text = 'Run';
props.onClick = this.run;
props.onClick = window.ElectronFiddle.app.runner.run;
props.icon = 'play';
}
} else {
return null;
props.text = 'Checking status';
props.icon = <Spinner size={16} />;
}
return <Button {...props} />;
}
/**
* Stop a currently running Electron fiddle.
*/
public async stop() {
if (this.child) {
this.child.kill();
this.setState({
isRunning: false
});
}
}
/**
* Analyzes the editor's JavaScript contents for modules
* and installs them.
*
* @param {EditorValues} values
* @param {string} dir
* @returns {Promise<void>}
*/
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
const modules = await findModulesInEditors(values);
const { appState } = this.props;
if (modules && modules.length > 0) {
if (!(await getIsNpmInstalled())) {
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
message += `but we could not find npm. Fiddle requires Node.js and npm `;
message += `to support the installation of modules not included in `;
message += `Electron. Please visit https://nodejs.org to install Node.js `;
message += `and npm.`;
appState.pushOutput(message, { isNotPre: true });
return;
}
appState.pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
appState.pushOutput(await installModules({ dir }, ...modules));
}
}
/**
* Execute Electron.
*
* @param {string} dir
* @param {string} version
* @returns {Promise<void>}
* @memberof Runner
*/
public async execute(dir: string): Promise<void> {
const { appState } = this.props;
const { version, pushOutput, versions } = appState;
const isLocal = versions[version] && versions[version].source === ElectronVersionSource.local;
const electronDir = isLocal ? versions[version].localPath : undefined;
const binaryPath = appState.binaryManager.getElectronBinaryPath(version, electronDir);
console.log(`Runner: Binary ${binaryPath} ready, launching`);
this.child = spawn(binaryPath, [ dir, '--inspect' ]);
this.setState({ 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.on('close', (code) => {
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
: `.`;
pushOutput(`Electron exited${withCode}`);
this.setState({ isRunning: false });
this.child = null;
window.ElectronFiddle.app.fileManager.cleanup(dir);
});
}
/**
* Save files to temp, logging to the Fiddle terminal while doing so
*
* @param {PackageJsonOptions} options
* @param {...Array<FileTransform>} transforms
* @returns {(Promise<string | null>)}
* @memberof Runner
*/
public async saveToTemp(
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string | null> {
const { fileManager } = window.ElectronFiddle.app;
const { pushOutput, pushError } = this.props.appState;
try {
pushOutput(`Saving files to temp directory...`);
const dir = await fileManager.saveToTemp(options, ...transforms);
pushOutput(`Saved files to ${dir}`);
return dir;
} catch (error) {
pushError('Failed to save files.', error);
}
return null;
}
/**
* Installs modules in a given directory (we're basically
* just running "npm install")
*
* @param {string} dir
* @returns
* @memberof Runner
*/
public async npmInstall(dir: string): Promise<boolean> {
try {
this.props.appState.pushOutput(`Now running "npm install..."`);
this.props.appState.pushOutput(await installModules({ dir }));
return true;
} catch (error) {
this.props.appState.pushError('Failed to run "npm install".', error);
}
return false;
}
/**
* Uses electron-forge to either package or make the current fiddle
*
* @param {ForgeCommands} operation
* @returns {Promise<boolean>}
* @memberof Runner
*/
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
const options = { includeDependencies: true, includeElectron: true };
const { dotfilesTransform } = await import('../transforms/dotfiles');
const { forgeTransform } = await import('../transforms/forge');
const { appState } = this.props;
const { pushError, pushOutput } = appState;
const strings = operation === ForgeCommands.MAKE
? [ 'Creating installers for', 'Binary' ]
: [ 'Packaging', 'Installers' ];
appState.isConsoleShowing = true;
pushOutput(`📦 ${strings[0]} current Fiddle...`);
if (!(await getIsNpmInstalled())) {
let message = `Error: Could not find npm. Fiddle requires Node.js and npm `;
message += `to compile packages. Please visit https://nodejs.org to install `;
message += `Node.js and npm.`;
appState.pushOutput(message, { isNotPre: true });
return false;
}
// Save files to temp
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
if (!dir) return false;
// Files are now saved to temp, let's install Forge and dependencies
if (!(await this.npmInstall(dir))) return false;
// Cool, let's run "package"
try {
console.log(`Now creating ${strings[1].toLowerCase()}...`);
pushOutput(await npmRun({ dir }, operation));
pushOutput(`${strings[1]} successfully created.`, { isNotPre: true });
} catch (error) {
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
return false;
}
const { shell } = await import('electron');
shell.showItemInFolder(path.join(dir, 'out'));
return true;
}
/**
* Actually run the fiddle.
*
* @returns {Promise<boolean>}
*/
public async run(): Promise<boolean> {
const { appState } = this.props;
const { fileManager, getValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { binaryManager, version, versions } = appState;
appState.isConsoleShowing = true;
const isLocal = versions[version] && versions[version].source === ElectronVersionSource.local;
const isDownloaded = isLocal || await binaryManager.getIsDownloaded(version);
const values = await getValues(options);
const dir = await this.saveToTemp(options);
if (!dir) return false;
try {
await this.installModulesForEditor(values, dir);
} catch (error) {
console.error('Runner: Could not install modules', error);
fileManager.cleanup(dir);
return false;
}
if (!isDownloaded) {
console.warn(`Runner: Binary ${version} not ready`);
let message = `Could not start fiddle: `;
message += `Electron ${version} not downloaded yet. `;
message += `Please wait for it to finish downloading `;
message += `before running the fiddle.`;
appState.pushOutput(message, { isNotPre: true });
fileManager.cleanup(dir);
return false;
}
this.execute(dir);
return true;
return <Button {...props} type={undefined} />;
}
}
@@ -1,95 +1,9 @@
import { Button, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { ButtonGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { AppState } from '../state';
import { getReleaseChannel } from '../versions';
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {ElectronVersion} { source, state }
* @returns {string}
*/
function getItemLabel({ source, state }: ElectronVersion): string {
let label = '';
if (source === ElectronVersionSource.local) {
label = name;
} else {
if (state === ElectronVersionState.unknown) {
label = `Not downloaded`;
} else if (state === ElectronVersionState.ready) {
label = `Downloaded`;
} else if (state === ElectronVersionState.downloading) {
label = `Downloading`;
}
}
return label;
}
/**
* Helper method: Returns the <Select /> icon for an Electron
* version.
*
* @param {ElectronVersion} { state }
* @returns
*/
function getItemIcon({ state }: ElectronVersion) {
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
*/
const filterItem: ItemPredicate<ElectronVersion> = (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
*/
const renderItem: ItemRenderer<ElectronVersion> = (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;
}
import { VersionSelect } from './version-select';
export interface VersionChooserProps {
appState: AppState;
@@ -98,56 +12,18 @@ export interface VersionChooserProps {
/**
* 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 VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
export const VersionChooser = observer((props: VersionChooserProps) => {
const { currentElectronVersion, Bisector, setVersion } = props.appState;
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.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 { version, versions } = this.props.appState;
const selectedVersion = versions[version];
if (!selectedVersion) return null;
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(selectedVersion)}
/>
</ElectronVersionSelect>
);
}
}
return (
<ButtonGroup>
<VersionSelect
appState={props.appState}
onVersionSelect={({ version }) => setVersion(version)}
currentVersion={currentElectronVersion}
disabled={!!Bisector}
/>
</ButtonGroup>
);
});
+13
View File
@@ -4,6 +4,8 @@ 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 { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
import { VersionChooser } from './commands-version-chooser';
@@ -27,6 +29,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
public render() {
const { appState } = this.props;
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className='commands'>
@@ -35,6 +38,15 @@ export class Commands extends React.Component<CommandsProps, {}> {
<VersionChooser appState={appState} />
<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 +54,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
text='Console'
onClick={appState.toggleConsole}
/>
<EditorDropdown appState={appState} />
</ControlGroup>
</div>
<div>
@@ -0,0 +1,164 @@
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 { GenericDialogType } from '../../../src/interfaces';
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.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Error: ${error}, please pick a different file.`
});
this.props.appState.isGenericDialogShowing = 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;
}
}
+44 -38
View File
@@ -5,8 +5,11 @@ import * as path from 'path';
import * as React from 'react';
import * as semver from 'semver';
import { NpmVersion } from '../../interfaces';
import { Version } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { getElectronNameForPlatform } from '../../utils/electron-name';
import { getIsDownloaded } from '../binary';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface AddVersionDialogProps {
@@ -16,7 +19,7 @@ export interface AddVersionDialogProps {
export interface AddVersionDialogState {
isValidElectron: boolean;
isValidVersion: boolean;
file?: File;
folderPath?: string;
version: string;
}
@@ -39,8 +42,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 +58,10 @@ 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;
const { binaryManager } = this.props.appState;
const file = files && files[0] ? files[0] : undefined;
public async setFolderPath(folderPath: string) {
const isValidElectron = !!await 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,
const toAdd: Version = {
localPath: folderPath,
version,
name
};
@@ -104,18 +108,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.reset();
}
/**
* Reset this component's state
*/
public reset(): void {
this.setState({
isValidElectron: false,
isValidVersion: false,
version: '',
file: undefined
});
}
get buttons() {
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
@@ -141,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
@@ -157,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}
@@ -175,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.`
@@ -212,4 +207,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
);
}
/**
* Reset this component's state
*/
private reset(): void {
this.setState({
isValidElectron: false,
isValidVersion: false,
version: '',
folderPath: undefined
});
}
}
+237
View File
@@ -0,0 +1,237 @@
import { Button, ButtonGroup, Callout, Dialog, Label } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion } from '../../interfaces';
import { Bisector } from '../bisect';
import { AppState } from '../state';
import { VersionSelect } from './version-select';
export interface BisectDialogProps {
appState: AppState;
}
export interface BisectDialogState {
startIndex: number;
endIndex: number;
allVersions: Array<RunnableVersion>;
showHelp?: boolean;
}
/**
* The "add version" dialog allows users to add custom builds of Electron.
*
* @class BisectDialog
* @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);
this.showHelp = this.showHelp.bind(this);
this.isEarliestItemDisabled = this.isEarliestItemDisabled.bind(this);
this.isLatestItemDisabled = this.isLatestItemDisabled.bind(this);
this.state = {
allVersions: this.props.appState.versionsToShow,
startIndex: 10,
endIndex: 0
};
}
public onBeginSelect(version: RunnableVersion) {
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
}
public onEndSelect(version: RunnableVersion) {
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;
}
/**
* Shows the additional help
*/
public showHelp() {
this.setState({ showHelp: true });
}
/**
* Can we get this show on the road?
*/
get canSubmit(): boolean {
return this.state.startIndex > this.state.endIndex;
}
/**
* Renders the buttons
*/
get buttons() {
return [
(
<Button
icon='play'
key='submit'
disabled={!this.canSubmit}
onClick={this.onSubmit}
text='Begin'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
/**
* Renders the help
*/
get help() {
let moreHelp = (
<Button
icon='help'
text='Show help'
onClick={this.showHelp}
/>
);
if (this.state.showHelp) {
moreHelp = (
<>
<p>
First, write a fiddle that reproduces a bug or an issue. Then, select the earliest version to
start your search with. Typically, that's the "last known good" version that did not have the bug.
Then, select that latest version to end the search with, usually the "first known bad" version.
</p>
<p>
Once you begin your bisect, Fiddle will run your fiddle with a number of Electron versions, closing
in on the version that introduced the bug. Once completed, you will know which Electron version
introduced your issue.
</p>
</>
);
}
return (
<Callout style={{ marginTop: 0, marginBottom: '1rem' }}>
<p>
A "bisect" is a popular method <a href='https://git-scm.com/docs/git-bisect' target='_blank'>
borrowed from <code>git</code></a> for learning which version of Electron introduced a bug. This
tool helps you perform a bisect.
</p>
{moreHelp}
</Callout>
);
}
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'>
{this.help}
<Label>
Earliest Version (Last "known good" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[startIndex]}
appState={this.props.appState}
onVersionSelect={this.onBeginSelect}
itemDisabled={this.isEarliestItemDisabled}
/>
</ButtonGroup>
</Label>
<Label>
Latest Version (First "known bad" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[endIndex]}
appState={this.props.appState}
onVersionSelect={this.onEndSelect}
itemDisabled={this.isLatestItemDisabled}
/>
</ButtonGroup>
</Label>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
/**
* Should an item in the "earliest version" dropdown be disabled?
*
* @param {RunnableVersion} version
* @returns {boolean}
*/
public isEarliestItemDisabled(version: RunnableVersion): boolean {
const { allVersions, endIndex } = this.state;
// In the array, "newer" versions will have a lower index.
// 0: 5.0.0
// 1: 4.0.0
// 2: 3.0.0
// ...
return allVersions.indexOf(version) < endIndex + 1;
}
/**
* Should an item in the "latest version" dropdown be disabled?
*
* @param {RunnableVersion} version
* @returns {boolean}
*/
public isLatestItemDisabled(version: RunnableVersion): boolean {
const { allVersions, startIndex } = this.state;
return allVersions.indexOf(version) > startIndex - 1;
}
}
@@ -0,0 +1,73 @@
import { Alert, IconName, Intent} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { GenericDialogType } from '../../../src/interfaces';
import { AppState } from '../state';
export interface GenericDialogProps {
appState: AppState;
}
export interface GenericDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class GenericDialog
* @extends {React.Component<GenericDialogProps, GenericDialogState>}
*/
@observer
export class GenericDialog extends React.Component<GenericDialogProps, GenericDialogState> {
constructor(props: GenericDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.genericDialogLastResult = result;
this.props.appState.toggleGenericDialog();
}
public render() {
const { isGenericDialogShowing, genericDialogOptions } = this.props.appState;
const {type, ok, cancel, label} = genericDialogOptions;
let intent: Intent;
let icon: IconName;
switch (type) {
case GenericDialogType.warning:
intent = Intent.DANGER;
icon = 'warning-sign';
break;
case GenericDialogType.confirm:
intent = Intent.PRIMARY;
icon = 'help';
break;
case GenericDialogType.success:
intent = Intent.SUCCESS;
icon = 'info-sign';
break;
default:
intent = Intent.NONE;
icon = 'help';
break;
}
return (
<Alert
isOpen={isGenericDialogShowing}
onClose={this.onClose}
icon={icon}
confirmButtonText={ok}
cancelButtonText={cancel}
intent={intent}
>
<p>{label}</p>
</Alert>
);
}
}
+9 -13
View File
@@ -12,7 +12,7 @@ export interface TokenDialogProps {
}
export interface TokenDialogState {
tokenInput?: string;
tokenInput: string;
verifying: boolean;
error: boolean;
}
@@ -37,7 +37,9 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
this.state = {
verifying: false,
error: false,
tokenInput: ''
};
this.onSubmitToken = this.onSubmitToken.bind(this);
this.openGenerateTokenExternal = this.openGenerateTokenExternal.bind(this);
this.onTokenInputFocused = this.onTokenInputFocused.bind(this);
@@ -53,31 +55,25 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
*/
public async onSubmitToken(): Promise<void> {
if (!this.state.tokenInput) return;
this.setState({
verifying: true,
});
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 });
this.props.appState.gitHubToken = null;
return;
}
this.props.appState.gitHubToken = this.state.tokenInput;
this.props.appState.isTokenDialogShowing = false;
this.setState({ verifying: false, error: false });
}
/**
@@ -1,52 +0,0 @@
import { Alert, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface WarningDialogProps {
appState: AppState;
}
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.
*
* @export
* @class WarningDialog
* @extends {React.Component<WarningDialogProps, WarningDialogState>}
*/
@observer
export class WarningDialog extends React.Component<WarningDialogProps, WarningDialogState> {
constructor(props: WarningDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.warningDialogLastResult = result;
this.props.appState.toogleWarningDialog();
}
public render() {
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isWarningDialogShowing}
onClose={this.onClose}
icon='warning-sign'
confirmButtonText={warningDialogTexts.ok}
cancelButtonText={warningDialogTexts.cancel}
intent={Intent.DANGER}
>
<p>{warningDialogTexts.label}</p>
</Alert>
);
}
}
-111
View File
@@ -1,111 +0,0 @@
import * as React from 'react';
import { ArrowPosition } from '../../interfaces';
import { classNames } from '../../utils/classnames';
import { sortButtons } from '../../utils/sort-buttons';
export interface DialogProps {
buttons?: Array<JSX.Element> | null;
style?: React.CSSProperties;
isCentered?: boolean;
className?: string;
isShowing?: boolean;
isShowingBackdrop?: boolean;
arrow?: ArrowPosition;
onClose?: () => void;
onConfirm?: () => void;
}
/**
* A generic dialog class for Fiddle.
*
* @class Dialog
* @extends {React.Component<DialogProps, {}>}
*/
export class Dialog extends React.Component<DialogProps, {}> {
public static defaultProps: Partial<DialogProps> = {
isCentered: false,
isShowing: true,
isShowingBackdrop: false
};
constructor(props: DialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
this.onConfirm = this.onConfirm.bind(this);
}
/**
* Closes the dialog
*/
public onClose() {
if (this.props.onClose) this.props.onClose();
}
/**
* Confirms the dialog
*/
public onConfirm() {
if (this.props.onConfirm) this.props.onConfirm();
}
/**
* Renders the dialog's buttons, using a default "Ok"
* if none were passed.
*
* @returns {Array<JSX.Element> | JSX.Element}
*/
public renderButtons(): Array<JSX.Element | null> | JSX.Element {
const { buttons, onClose } = this.props;
const closeButton = onClose
? <button key='cancel' className='btn-close' onClick={this.onClose}>Cancel</button>
: null;
const okButton = (
<button key='ok' className='btn-ok' onClick={this.onConfirm}>Ok</button>
);
return (
<div className='dialog-buttons'>
{sortButtons(buttons! || [ closeButton, okButton ])}
</div>
);
}
/**
* Optionally renders a backdrop that closes the
* dialog if clicked.
*
* @returns {JSX.Element | null}
*/
public renderBackdrop(): JSX.Element | null {
const { isShowingBackdrop } = this.props;
if (!isShowingBackdrop) return null;
return (
<div key='drop' className='dialogDrop' onClick={this.onClose} />
);
}
public render() {
const { isShowing, isCentered, style, children, className, arrow } = this.props;
const arrowClass = arrow ? `arrow arrow-${arrow}` : '';
const parsedClassName = classNames(
'dialog',
arrowClass,
{ centered: isCentered },
className
);
return isShowing ? (
<>
{this.renderBackdrop()}
<div style={style} className={parsedClassName}>
{children}
{this.renderButtons()}
</div>
</>
) : null;
}
}
+22 -3
View File
@@ -2,9 +2,11 @@ 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 { GenericDialog } from './dialog-generic';
import { TokenDialog } from './dialog-token';
import { WarningDialog } from './dialog-warning';
import { Settings } from './settings';
export interface DialogsProps {
@@ -21,7 +23,14 @@ 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,
isGenericDialogShowing
} = 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 genericDialog = isGenericDialogShowing
? <GenericDialog appState={appState} />
: null;
return (
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
<WarningDialog appState={appState} />
{maybeMonaco}
{maybeBisect}
{genericDialog}
</div>
);
}
+83 -12
View File
@@ -4,17 +4,19 @@
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;
setFocused: (id: EditorId) => void;
}
export class Editor extends React.Component<EditorProps> {
@@ -27,15 +29,24 @@ export class Editor extends React.Component<EditorProps> {
constructor(props: EditorProps) {
super(props);
this.language = props.id === 'html' ? 'html' : 'javascript';
switch (props.id) {
case 'html':
this.language = 'html';
break;
case 'css':
this.language = 'css';
break;
default:
this.language = 'javascript';
}
}
public shouldComponentUpdate() {
return false;
}
public componentDidMount() {
this.initMonaco();
public async componentDidMount() {
await this.initMonaco();
}
public componentWillUnmount() {
@@ -48,11 +59,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 +78,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 +86,17 @@ 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);
// mark this editor as focused whenever it is
this.editor.onDidFocusEditorText(() => {
const { id, setFocused } = this.props;
setFocused(id);
});
await this.editorDidMount(this.editor);
}
}
@@ -83,6 +105,7 @@ export class Editor extends React.Component<EditorProps> {
*/
public destroyMonaco() {
if (typeof this.editor !== 'undefined') {
console.log('Editor: Disposing');
this.editor.dispose();
}
}
@@ -90,4 +113,52 @@ export class Editor extends React.Component<EditorProps> {
public render() {
return <div className='editorContainer' ref={this.containerRef} />;
}
/**
* Create a model and attach it to the editor
*
* @private
* @param {string} value
*/
private createModel(value: string) {
const { monaco } = this.props;
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2
});
this.editor.setModel(model);
}
/**
* Sets the content on the editor, including the model and the view state.
*
* @private
* @memberof Editor
*/
private async setContent() {
const { appState, id } = this.props;
const { version } = appState;
const backup = appState.getAndRemoveEditorValueBackup(id);
if (backup) {
console.log(`Editor: Backup found, restoring state`);
if (backup.viewState) {
this.editor.restoreViewState(backup.viewState);
}
// If there's a model, use the model. No model? Use the value
if (backup.model) {
this.editor.setModel(backup.model);
} else {
this.createModel(backup.value ?? '');
}
} else {
const value = await getContent(id, version);
this.createModel(value);
}
}
}
@@ -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'
/>
);
}
}
+203 -61
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,13 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
wordWrap: 'on'
};
const ViewIdMosaic = Mosaic.ofType<EditorId>();
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>();
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)',
css: 'Stylesheet (styles.css)',
docsDemo: 'Docs & Demos',
};
export interface EditorsProps {
@@ -48,16 +44,36 @@ export interface EditorsState {
monaco?: typeof MonacoType;
isMounted?: boolean;
monacoOptions: MonacoType.editor.IEditorOptions;
focused?: EditorId;
}
@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.setFocused = this.setFocused.bind(this);
this.state = { monacoOptions: defaultMonacoOptions };
this.loadMonaco();
}
public componentDidMount() {
/**
* Executed right after the component mounts. We'll setup the IPC listeners here.
*
* @memberof Editors
*/
public async componentDidMount() {
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
this.executeCommand(cmd);
});
@@ -65,26 +81,28 @@ 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.TOGGLE_SOFT_WRAP, async (_event) => {
this.toggleEditorOption('wordWrap');
});
ipcRendererManager.on(IpcEvents.TOGGLE_MINI_MAP, async (_event) => {
this.toggleEditorOption('minimap.enabled');
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
this.toggleEditorOption(cmd);
});
this.setState({ isMounted: true });
await this.loadMonaco();
this.props.appState.isUnsaved = false;
}
public componentWillUnmount() {
this.disposeLayoutAutorun();
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_EXECUTE_COMMAND);
ipcRendererManager.removeAllListeners(IpcEvents.FS_NEW_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_TOGGLE_OPTION);
}
/**
@@ -107,9 +125,9 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
}
}
public toggleEditorOption(path: string) {
if (!window || !window.ElectronFiddle || !window.ElectronFiddle.editors) {
return;
public toggleEditorOption(path: string): boolean {
if (!window.ElectronFiddle.editors) {
return false;
}
try {
@@ -120,21 +138,128 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
setAtPath(path, newOptions, toggleMonaco(currentSetting));
Object.keys(window.ElectronFiddle.editors)
.forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null
= window.ElectronFiddle.editors[key];
.forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null
= window.ElectronFiddle.editors[key];
if (editor) {
editor.updateOptions(newOptions);
}
});
if (editor) {
editor.updateOptions(newOptions);
}
});
this.setState({ monacoOptions: newOptions });
return true;
} catch (error) {
console.warn(`Editors: Could not toggle property ${path}`, error);
return false;
}
}
/**
* 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 { appState } = this.props;
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
? <DocsDemoGoHomeButton id={id} appState={appState} />
: null;
// only show toolbar controls if we have more than 1 visible editor
// Mosaic arrangement is type string if 1 editor, object otherwise
const toolbarControlsMaybe =
(typeof appState.mosaicArrangement !== 'string') &&
(
<>
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
</>
);
return (
<div>
{/* Left */}
<div>
<h5>
{title}
</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className='mosaic-controls'>
{docsDemoGoHomeMaybe}
{toolbarControlsMaybe}
</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}
setFocused={this.setFocused}
/>
);
}
public render() {
const { appState } = this.props;
const { monaco } = this.state;
@@ -142,27 +267,31 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
if (!monaco) return null;
return (
<ViewIdMosaic
onChange={updateEditorLayout}
// tslint:disable-next-line:jsx-no-multiline-js
renderTile={(id, path) => (
<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>
className={`focused__${this.state.focused}`}
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
* mobx state with a gigantic Monaco tree.
*
* @private
*/
private async loadMonaco(): Promise<void> {
public async loadMonaco() {
const { app } = window.ElectronFiddle;
const loader = require('monaco-loader');
const monaco = app.monaco || await loader();
@@ -172,11 +301,24 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
}
if (!this.state || !this.state.isMounted) {
this.state = { monaco, monacoOptions: defaultMonacoOptions };
this.setState({
monaco,
monacoOptions: defaultMonacoOptions
});
} else {
this.setState({ monaco });
}
activateTheme(monaco, undefined, this.props.appState.theme);
await activateTheme(monaco, undefined, this.props.appState.theme);
}
/**
* Sets the currently-focused editor. This will impact the editor's
* z-index, ensuring that its intellisense menus don't get clipped
* by the other editor windows.
* @param id Editor ID
*/
public setFocused(id: EditorId): void {
this.setState({ focused: id });
}
}
-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});
}
}
+31 -4
View File
@@ -1,11 +1,16 @@
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;
// Used to keep testing conform
renderTimestamp?: (ts: number) => string;
}
@@ -18,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) {
@@ -27,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;
@@ -70,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
@@ -80,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>
);
+39 -12
View File
@@ -1,22 +1,45 @@
import { Callout, Card } from '@blueprintjs/core';
import { shell } from 'electron';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as React from 'react';
import { AppState } from '../state';
export interface CreditsSettingsProps {
appState: AppState;
contributors?: Array<any>;
}
export interface CreditsSettingsState {
contributors: Array<Contributor>;
}
export interface Contributor {
url: string;
api: string;
login: string;
avatar: string;
name: string;
bio: string;
location: string;
}
/**
* Settings content to manage Credits-related preferences.
*
* @class CreditsSettings
* @extends {React.Component<CreditsSettingsProps, {}>}
* @extends {React.Component<CreditsSettingsProps, CreditsSettingsState>}
*/
export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
private contributors: Array<any> | null = null;
export class CreditsSettings extends React.Component<CreditsSettingsProps, CreditsSettingsState> {
constructor(props: CreditsSettingsProps) {
super(props);
this.state = {
contributors: []
};
this.getContributors();
}
/**
* Renders a list of contributors of Electron Fiddle.
@@ -24,15 +47,9 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
* @returns {Array<JSX.Element>}
*/
public renderContributors(): Array<JSX.Element> {
this.contributors = this.contributors
|| this.props.contributors
|| require('../../../static/contributors.json');
const { contributors } = this.state;
if (!this.contributors || !Array.isArray(this.contributors)) {
return [];
}
return this.contributors.map(({ name, avatar, url, login, location, bio }) => {
return contributors.map(({ name, avatar, url, login, location, bio }) => {
const maybeLocation = location
? <p className='location'>📍 {location}</p>
: null;
@@ -73,4 +90,14 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
</div>
);
}
public async getContributors() {
try {
const contributorsFile = path.join(__dirname, '../../static/contributors.json');
const contributors = await fs.readJSON(contributorsFile);
this.setState({ contributors });
} catch (error) {
console.warn(`CreditsSettings: Fetching contributors failed`, error);
}
}
}
+130 -50
View File
@@ -7,12 +7,13 @@ import {
HTMLTable,
IButtonProps,
Icon,
IconName
IconName,
Tooltip
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
import { normalizeVersion } from '../../utils/normalize-version';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { AppState } from '../state';
@@ -41,6 +42,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 +56,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.
*
@@ -66,9 +86,9 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
const { appState } = this.props;
if (!checked) {
appState.versionsToShow = appState.versionsToShow.filter((c) => c !== id);
appState.channelsToShow = appState.channelsToShow.filter((c) => c !== id);
} else {
appState.versionsToShow.push(id as ElectronReleaseChannel);
appState.channelsToShow.push(id as ElectronReleaseChannel);
}
}
@@ -120,7 +140,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 +196,103 @@ 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 = (channel: ElectronReleaseChannel) => {
return appState.versionsToShow.includes(channel);
const getIsChecked = (state: VersionState) => {
return appState.statesToShow.includes(state);
};
return (
<Callout>
<FormGroup
label='Include Electron versions from these release channels:'
<FormGroup
label='Include Electron versions that are:'
>
<Tooltip
content='Always enabled'
position='bottom'
intent='primary'
>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.stable)}
label='Stable'
id='Stable'
onChange={this.handleChannelChange}
checked={getIsChecked(VersionState.ready)}
label='Ready'
id='ready'
onChange={this.handleStateChange}
inline={true}
disabled={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>
</Tooltip>
<Checkbox
checked={getIsChecked(VersionState.downloading)}
label='Downloading'
id='downloading'
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.channelsToShow.includes(channel);
};
const getIsCurrentVersionReleaseChannel = (channel: ElectronReleaseChannel) => {
return getReleaseChannel(appState.version) === channel;
};
const channels = {
stable: ElectronReleaseChannel.stable,
beta: ElectronReleaseChannel.beta,
nightly: ElectronReleaseChannel.nightly,
unsupported: ElectronReleaseChannel.unsupported
};
return (
<FormGroup
label='Include Electron versions from these release channels:'
>
{
// tslint:disable-next-line:jsx-no-multiline-js
Object.entries(channels).map(([_, channel]) => (
<Tooltip
content={`Can't disable channel of selected version (${appState.version})`}
disabled={!getIsCurrentVersionReleaseChannel(channel)}
position='bottom'
intent='primary'
key={channel}
>
<Checkbox
checked={getIsChecked(channel)}
label={channel}
id={channel}
onChange={this.handleChannelChange}
disabled={getIsCurrentVersionReleaseChannel(channel)}
inline={true}
/>
</Tooltip>
))
}
</FormGroup>
);
}
@@ -254,11 +329,16 @@ 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, channelsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
if (!channelsToShow.includes(getReleaseChannel(item))) {
return null;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return null;
}
@@ -273,20 +353,20 @@ 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
* @param {RunnableVersion} item
* @returns {JSX.Element}
*/
private renderHumanState(item: ElectronVersion): JSX.Element {
private renderHumanState(item: RunnableVersion): JSX.Element {
const { state } = item;
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';
}
@@ -303,10 +383,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
*
* @private
* @param {string} key
* @param {ElectronVersion} item
* @param {RunnableVersion} item
* @returns {JSX.Element}
*/
private renderAction(key: string, item: ElectronVersion): JSX.Element {
private renderAction(key: string, item: RunnableVersion): JSX.Element {
const { state, source } = item;
const { appState } = this.props;
const buttonProps: IButtonProps = {
@@ -318,7 +398,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
if (state === 'ready') {
buttonProps.onClick = () => appState.removeVersion(key);
buttonProps.icon = 'trash';
buttonProps.text = source === ElectronVersionSource.local
buttonProps.text = source === VersionSource.local
? 'Remove'
: 'Delete';
} else if (state === 'downloading') {
@@ -331,9 +411,9 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
buttonProps.loading = false;
buttonProps.text = 'Download';
buttonProps.icon = 'cloud-download';
buttonProps.onClick = () => appState.setVersion(key);
buttonProps.onClick = () => appState.downloadVersion(key);
}
return <Button {...buttonProps} />;
return <Button {...buttonProps} type={undefined} />;
}
}
@@ -0,0 +1,131 @@
import { Callout, Checkbox, FormGroup, InputGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ExecutionSettingsProps {
appState: AppState;
}
/**
* Settings content to manage execution-related preferences.
*
* @class ExecutionSettings
* @extends {React.Component<ExecutionSettingsProps, {}>}
*/
@observer
export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {}> {
constructor(props: ExecutionSettingsProps) {
super(props);
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
}
/**
* Handles a change on whether or not the user data dir should be deleted
* after a run.
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleDeleteDataChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
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;
}
/**
* Handles a change in the execution flags run with the Electron executable
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleExecutionFlagChange(
event: React.FormEvent<HTMLInputElement>
) {
const { value } = event.currentTarget;
const flags = value.split('|');
this.props.appState.executionFlags = flags;
}
public render() {
const {
isKeepingUserDataDirs,
isEnablingElectronLogging,
executionFlags = []
} = 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>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>
<br />
<Callout>
<FormGroup label={electronLoggingLabel}>
<Checkbox
checked={isEnablingElectronLogging}
label='Enable advanced Electron logging.'
onChange={this.handleElectronLoggingChange}
/>
</FormGroup>
</Callout>
<br />
<Callout>
<FormGroup>
<p>
Electron allows starting the executable with <a
href='https://www.electronjs.org/docs/api/command-line-switches'
>
user-provided flags
</a>
, such as '--js-flags=--expose-gc'. Those can be added here as bar-separated (|)
flags to run when you start your Fiddles.
</p>
<br />
<InputGroup
placeholder='--js-flags=--expose-gc|--lang=es'
value={executionFlags.join('|')}
onChange={this.handleExecutionFlagChange}
/>
</FormGroup>
</Callout>
</div>
);
}
}
@@ -19,10 +19,10 @@ const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
* version.
*
* @param {string} query
* @param {ElectronVersion} { version }
* @param {RunnableVersion} { version }
* @returns
*/
const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
return name.toLowerCase().includes(query.toLowerCase());
};
@@ -31,11 +31,11 @@ const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {ElectronVersion} item
* @param {RunnableVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
@@ -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>
);
}
}
@@ -16,23 +16,25 @@ export interface GitHubSettingsProps {
*/
@observer
export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
constructor(props: GitHubSettingsProps) {
super(props);
this.signIn = this.signIn.bind(this);
}
/**
* Render the "logged out" settings experience.
*
* @returns {JSX.Element}
*/
public renderNotSignedIn(): JSX.Element {
const signIn = () => {
this.props.appState.isTokenDialogShowing = true;
};
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={signIn} icon='log-in' text='Sign in'/>
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
</Callout>
);
}
@@ -72,4 +74,11 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
</div>
);
}
/**
* Simply shows the GitHub Token dialog.``
*/
private signIn() {
this.props.appState.isTokenDialogShowing = true;
}
}
@@ -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>
);
+31 -2
View File
@@ -1,21 +1,24 @@
import { Icon, MenuItem } from '@blueprintjs/core';
import { Icon, IconName, MenuItem } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { CreditsSettings } from './settings-credits';
import { ElectronSettings } from './settings-electron';
import { ExecutionSettings } from './settings-execution';
import { GeneralSettings } from './settings-general';
enum SettingsSections {
General = 'General',
Electron = 'Electron',
Execution = 'Execution',
Credits = 'Credits'
}
const settingsSections = [
SettingsSections.General,
SettingsSections.Electron,
SettingsSections.Execution,
SettingsSections.Credits
];
@@ -41,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);
}
/**
@@ -61,6 +74,10 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
return <ElectronSettings appState={appState} />;
}
if (section === SettingsSections.Execution) {
return <ExecutionSettings appState={appState} />;
}
if (section === SettingsSections.Credits) {
return <CreditsSettings appState={appState} />;
}
@@ -120,13 +137,25 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
* @param {SettingsSections} section
* @memberof Settings
*/
private getIconForSection(section: SettingsSections) {
private getIconForSection(section: SettingsSections): IconName {
if (section === SettingsSections.Credits) {
return 'heart';
} else if (section === SettingsSections.Electron) {
return 'floppy-disk';
} else if (section === SettingsSections.Execution) {
return 'play';
}
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>
);
}
+83 -69
View File
@@ -1,8 +1,8 @@
import { Button, Classes, Dialog } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { Dialog } from './dialog';
import { Tour, TourScriptStep, TourStepGetButtonParams } from './tour';
export interface WelcomeTourProps {
@@ -19,33 +19,33 @@ export interface WelcomeTourState {
*
* @returns {Set<TourScriptStep>}
*/
function getWelcomeTour(): Set<TourScriptStep> {
export function getWelcomeTour(): Set<TourScriptStep> {
return new Set([
{
name: 'fiddle-editors',
selector: 'div.mosaic-root',
title: '📝 Fiddle Editors',
content: (
<div>
<h4>📝 Fiddle Editors</h4>
<>
<p>
Electron Fiddle allows you to build little experiments and mini-apps with
Electron. Each Fiddle has three files: A main script, a renderer script,
and an HTML file.
Electron. Each Fiddle has at least three of these files: A main script, a
renderer script, a preload script, and an HTML file.
</p>
<p>
If you <code>require()</code> a module, Fiddle will install
it automatically. It will also automatically provide you with autocomplete
information for the <code>electron</code> module.
</p>
</div>
</>
)
},
{
name: 'select-versions',
selector: 'select.select-versions',
selector: 'button.version-chooser',
title: '📇 Choose an Electron Version',
content: (
<div>
<h4>📇 Choose an Electron Version</h4>
<>
<p>
Electron Fiddle knows about all released Electron versions, downloading
your versions automatically in the background.
@@ -54,27 +54,25 @@ function getWelcomeTour(): Set<TourScriptStep> {
Open the preferences to see all available versions and delete those previously
downloaded.
</p>
</div>
</>
)
},
{
name: 'button-run',
selector: 'button.button-run',
selector: '.button-run',
title: '🚀 Run Your Fiddle',
content: (
<div>
<h4>🚀 Run Your Fiddle</h4>
<p>
Hit this button to give your Fiddle a try and start it.
</p>
</div>
<p>
Hit this button to give your Fiddle a try and start it.
</p>
)
},
{
name: 'button-publish',
selector: 'button.button-publish',
selector: '.button-publish',
title: '🗺 Share Your Fiddle',
content: (
<div>
<h4>🗺 Share Your Fiddle</h4>
<>
<p>
Like what you've built? You can save your Fiddle as a public GitHub Gist,
allowing other users to load it by pasting the URL into the address bar.
@@ -85,41 +83,39 @@ function getWelcomeTour(): Set<TourScriptStep> {
You can also package your Fiddle as a standalone binary or as an installer
from the "Tasks" menu.
</p>
</div>
</>
)
},
{
name: 'first-time-electron',
selector: 'div.mosaic-root',
title: '👋 Getting Started With Electron?',
content: (
<div>
<h4>👋 Getting Started With Electron?</h4>
<p>
We've finished our tour of Electron Fiddle, but if this is your
first time using Electron, we could introduce you to its basics.
Interested?
</p>
</div>
<p>
We've finished our tour of Electron Fiddle, but if this is your
first time using Electron, we could introduce you to its basics.
Interested?
</p>
),
getButtons: ({ stop, advance }: TourStepGetButtonParams): Array<JSX.Element> => {
return [
<button key='btn-stop' onClick={stop}>I'm good!</button>,
<button key='btn-adv' onClick={advance}>Electron Basics</button>
<Button key='btn-stop' onClick={stop} text="I'm good!" icon='stop' />,
<Button key='btn-adv' onClick={advance} text='Electron Basics' icon='help' />
];
}
},
{
name: 'main-editor',
selector: 'div.mosaic-window.main',
title: '📝 Main Script',
content: (
<div>
<h4>📝 Main Script</h4>
<>
<p>
Every Electron app starts with a main script, very similar to how
a Node.js application is started. The main script runs in the "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
@@ -131,42 +127,40 @@ function getWelcomeTour(): Set<TourScriptStep> {
The default fiddle creates a new <code>BrowserWindow</code> and loads
an HTML file.
</p>
</div>
</>
)
},
{
name: 'html-editor',
selector: 'div.mosaic-window.html',
title: '📝 HTML',
content: (
<div>
<h4>📝 HTML</h4>
<p>
In the default fiddle, this HTML file is loaded in the
<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>
require()</code> like we would in Node.js.
</p>
</div>
<p>
In the default fiddle, this HTML file is loaded in the
&nbsp;<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
in a browser will work here, too. In addition, Electron allows you
to execute Node.js code. Take a close look at the
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
)
},
{
name: 'renderer-editor',
selector: 'div.mosaic-window.renderer',
title: '📝 Renderer Script',
content: (
<div>
<h4>📝 Renderer Script</h4>
<>
<p>
This is the script we just required from the HTML file. In here, you can
do anything that works in Node.js <i>and</i> anything that works in a browser.
</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>
</div>
</>
)
}
]);
@@ -206,6 +200,25 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
this.setState({ isTourStarted: true });
}
get buttons() {
return (
<>
<Button
key='cancel'
onClick={this.stopTour}
icon='cross'
text={`I'll figure it out`}
/>
<Button
key='ok'
onClick={this.startTour}
icon='presentation'
text='Show me around'
/>
</>
);
}
public render() {
const { isTourShowing } = this.props.appState;
const { isTourStarted } = this.state;
@@ -213,28 +226,29 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
if (!isTourShowing) return null;
if (!isTourStarted) {
const buttons = [
<button key='cancel' className='btn-close' onClick={this.stopTour}>I'll figure it out</button>,
<button key='ok' className='btn-ok' onClick={this.startTour}>Show me around</button>
];
return (
<Dialog
key='welcome-tour-dialog'
isCentered={true}
isShowing={true}
isShowingBackdrop={true}
buttons={buttons}
isOpen={true}
>
<h4>🙋 Hey There!</h4>
<p>
Welcome to Electron Fiddle! If you're new to the app,
we'd like to give you a brief tour of its features.
</p>
<p>
We won't show this dialog again, but you can always
find the tour in the Help menu.
</p>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>🙋 Hey There!</h4>
</div>
<div className={Classes.DIALOG_BODY}>
<p>
Welcome to Electron Fiddle! If you're new to the app,
we'd like to give you a brief tour of its features.
</p>
<p>
We won't show this dialog again, but you can always
find the tour in the Help menu.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{this.buttons}
</div>
</div>
</Dialog>
);
} else {
+50 -15
View File
@@ -1,10 +1,12 @@
import { Button, Classes, Dialog } from '@blueprintjs/core';
import * as React from 'react';
import { invertPosition, positionForRect } from '../../utils/position-for-rect';
import { Dialog } from './dialog';
import { positionForRect } from '../../utils/position-for-rect';
export interface TourScriptStep {
name: string;
selector: string;
title: string;
content: JSX.Element;
getButtons?: (handlers: TourStepGetButtonParams) => Array<JSX.Element>;
}
@@ -116,12 +118,29 @@ export class Tour extends React.Component<TourProps, TourState> {
// No? Fine! Are we at the end of the tour?
return this.props.tour.size === this.state.i
? [
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Finish Tour</button>
]
: [
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Stop Tour</button>,
<button key='btn-adv' className='btn-adv' onClick={this.advance}>Continue</button>
? [(
<Button
icon='tick-circle'
onClick={this.stop}
key='btn-stop'
text='Finish Tour'
/>
)] : [
(
<Button
icon='stop'
key='btn-stop'
onClick={this.stop}
text='Stop Tour'
/>
), (
<Button
icon='step-forward'
key='btn-adv'
onClick={this.advance}
text='Continue'
/>
)
];
}
@@ -142,13 +161,31 @@ export class Tour extends React.Component<TourProps, TourState> {
top: position.top,
left: position.left,
width: size.width - margin,
margin: `${margin}px`
margin: `${margin}px`,
zIndex: 1000,
};
const arrow = invertPosition(position.type);
return (
<Dialog key={step.name} buttons={buttons} style={style} arrow={arrow}>
{step.content}
<Dialog
key={step.name}
isOpen={true}
style={style}
portalClassName='tour-portal'
backdropProps={{ style: { visibility: 'hidden' }}}
>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>
{step.title}
</h4>
</div>
<div className={Classes.DIALOG_BODY}>
{step.content}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{buttons}
</div>
</div>
</Dialog>
);
}
@@ -161,9 +198,7 @@ export class Tour extends React.Component<TourProps, TourState> {
*/
private getStep(step: TourScriptStep): JSX.Element | null {
const { selector } = step;
const element = document.querySelector(selector);
if (!element) return null;
const element = document.querySelector(selector) || document.body!;
const rect = element.getBoundingClientRect();
const { top, left, width, height } = rect;
+134
View File
@@ -0,0 +1,134 @@
import { Button, IButtonGroupProps, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { AppState } from '../state';
const ElectronVersionSelect = Select.ofType<RunnableVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {RunnableVersion} { source, state }
* @returns {string}
*/
export function getItemLabel({ source, state, name }: RunnableVersion): string {
let label = '';
if (source === VersionSource.local) {
label = name || 'Local';
} else {
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 {RunnableVersion} { state }
* @returns
*/
export function getItemIcon({ state }: RunnableVersion) {
return state === 'ready'
? 'saved'
: state === 'downloading' ? 'cloud-download' : 'cloud';
}
/**
* Helper method: Returns the <Select /> predicate for an Electron
* version.
*
* @param {string} query
* @param {RunnableVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<RunnableVersion> = (query, { version }) => {
return version.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {RunnableVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<RunnableVersion> = (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 VersionSelectState {
value: string;
}
export interface VersionSelectProps {
appState: AppState;
disabled?: boolean;
currentVersion: RunnableVersion;
onVersionSelect: (version: RunnableVersion) => void;
buttonGroupProps?: IButtonGroupProps;
itemDisabled?: keyof RunnableVersion | ((item: RunnableVersion, index: number) => boolean);
}
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
*
* @class VersionSelect
* @extends {React.Component<VersionSelectProps, VersionSelectState>}
*/
@observer
export class VersionSelect extends React.Component<VersionSelectProps, VersionSelectState> {
public render() {
const { currentVersion, itemDisabled } = this.props;
const { version } = currentVersion;
return (
<ElectronVersionSelect
filterable={true}
items={this.props.appState.versionsToShow}
itemRenderer={renderItem}
itemPredicate={filterItem}
itemDisabled={itemDisabled}
onItemSelect={this.props.onVersionSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!this.props.disabled}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentVersion)}
disabled={!!this.props.disabled}
/>
</ElectronVersionSelect>
);
}
}
+36
View File
@@ -0,0 +1,36 @@
import { remote } from 'electron';
import * as path from 'path';
import { MosaicNode } from 'react-mosaic-component';
import { EditorId, MosaicId } from '../interfaces';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
// Reminder: When testing, this file is mocked in tests/setup.js
export const USER_DATA_PATH = remote.app.getPath('userData');
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'row',
first: EditorId.main,
second: {
direction: 'column',
first: EditorId.renderer,
second: EditorId.html,
}
};
export const DEFAULT_CLOSED_PANELS: Partial<Record<MosaicId, EditorBackup | true>> = {
docsDemo: true,
preload: getEditorBackup(EditorId.preload),
css: getEditorBackup(EditorId.css),
};
export const EMPTY_EDITOR_CONTENT = {
html: '<!-- Empty -->',
js: '// Empty',
css: '/* Empty */'
};
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 {
+80 -22
View File
@@ -2,30 +2,61 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { USER_DATA_PATH } from '../constants';
import { RunnableVersion, VersionSource } from '../interfaces';
import { callIn } from '../utils/call-in';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
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;
}
}
/**
* Removes the type definition for a given version
*
* @param version
*/
export async function 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;
}
}
}
/**
* Get the path for offline TypeScript definitions
*
@@ -48,27 +79,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: RunnableVersion): 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 +108,27 @@ export async function getTypeDefinitions(version: string): Promise<string | null
return typeDefs;
}
}
return null;
}
}
export async function getLocalVersionTypeDefs(version: RunnableVersion) {
if (version.source === VersionSource.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: RunnableVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -106,17 +145,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 === VersionSource.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: RunnableVersion) {
if (version.localPath) {
return path.join(
version.localPath,
'gen',
'electron',
'tsc',
'typings',
'electron.d.ts'
);
} else {
return null;
}
}
+50 -34
View File
@@ -1,11 +1,10 @@
import * as fsType from 'fs-extra';
import * as path from 'path';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
import { EditorValues, Files, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { 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';
@@ -18,6 +17,11 @@ export class FileManager {
this.openFiddle = this.openFiddle.bind(this);
this.saveFiddle = this.saveFiddle.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_TEMPLATE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE_FORGE);
ipcRendererManager.on(IpcEvents.FS_OPEN_FIDDLE, (_event, filePath) => {
this.openFiddle(filePath);
});
@@ -35,13 +39,19 @@ export class FileManager {
});
}
public async openTemplate(name: string) {
const values = await getTemplateValues(name);
this.setFiddle(values);
/**
* Opens a template.
*
* @param {string} templateName
* @memberof FileManager
*/
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
* Tries to open a fiddle
* Tries to open a fiddle.
*
* @param {string} filePath
* @memberof FileManager
@@ -53,33 +63,16 @@ 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)),
css: await this.readFile(path.join(filePath, STYLES_CSS_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});
}
/**
@@ -89,7 +82,7 @@ export class FileManager {
* @param {string} filePath
* @memberof FileManager
*/
public async saveFiddle(filePath: string, ...transforms: Array<FileTransform>) {
public async saveFiddle(filePath?: string, ...transforms: Array<FileTransform>) {
const { localPath } = this.appState;
const pathToSave = filePath || localPath;
@@ -101,10 +94,14 @@ export class FileManager {
const files = await this.getFiles(undefined, ...transforms);
for (const [fileName, content] of files) {
try {
await this.saveFile(path.join(pathToSave, fileName), content);
} catch (error) {
console.warn(`FileManager: Failed to save file`, { fileName, error });
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);
}
}
@@ -113,7 +110,6 @@ export class FileManager {
}
this.appState.isUnsaved = false;
window.ElectronFiddle.app.setupUnsavedOnChangeListener();
}
}
@@ -128,12 +124,14 @@ 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(STYLES_CSS_NAME, values.css);
output.set(PACKAGE_NAME, values.package!);
for (const transform of transforms) {
@@ -236,4 +234,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]);
}
}
}
+1
View File
@@ -15,6 +15,7 @@ export class IpcRendererManager extends EventEmitter {
super();
ipcRendererEvents.forEach((name) => {
ipcRenderer.removeAllListeners(name);
ipcRenderer.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
}
+10 -5
View File
@@ -1,6 +1,8 @@
import { EditorValues } from '../interfaces';
import { exec } from '../utils/exec';
const { builtinModules } = require('module');
export interface NpmOperationOptions {
dir: string;
}
@@ -11,12 +13,12 @@ export let isInstalled: boolean | null = null;
/* perhaps we can expose this to the settings module?*/
const ignoredModules: Array<string> = [
'electron',
// tslint:disable-next-line:no-submodule-imports
...require('builtin-modules/static')
'original-fs',
...builtinModules
];
/* regular expression to both match and extract module names */
const requiregx = /require\(['"](.*?)['"]\)/gm;
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
@@ -91,8 +93,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 */
+273
View File
@@ -0,0 +1,273 @@
import { Octokit } from '@octokit/rest';
import { when } from 'mobx';
import { EditorId, EditorValues, GenericDialogType } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-electron-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 { path, ref } = exampleInfo;
const prettyName = path.replace('docs/fiddles/', '');
const ok = await this.verifyRemoteLoad(`'${prettyName}' example from the Electron docs for version ${ref}`);
if (!ok) return;
this.fetchExampleAndLoad(ref, path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const { id } = gistInfo;
const ok = await this.verifyRemoteLoad(`gist`);
if (!ok) return;
this.fetchGistAndLoad(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),
css: await getContent(EditorId.css, this.appState.version)
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
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; })
);
case STYLES_CSS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.css = 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),
css: this.getContentOrEmpty(gist, STYLES_CSS_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.channelsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
if (!ok) return false;
this.appState.channelsToShow.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): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.confirm,
label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
return !!this.appState.genericDialogLastResult;
}
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `You're loading an example with a version of Electron with an unincluded release
channel (${channel}). Do you want to enable the release channel to load the
version of Electron from the example?`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
return !!this.appState.genericDialogLastResult;
}
/**
* 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.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
} else {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined
});
}
this.appState.toggleGenericDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
+288
View File
@@ -0,0 +1,288 @@
import { ChildProcess, spawn } from 'child_process';
import * as path from 'path';
import { EditorValues, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { PackageJsonOptions } from '../utils/get-package';
import { maybePlural } from '../utils/plural-maybe';
import { getElectronBinaryPath, getIsDownloaded } from './binary';
import { ipcRendererManager } from './ipc';
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
import { AppState } from './state';
export enum ForgeCommands {
PACKAGE = 'package',
MAKE = 'make'
}
export class Runner {
public child: ChildProcess | null = null;
constructor(private readonly appState: AppState) {
this.run = this.run.bind(this);
this.stop = this.stop.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_RUN);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_PACKAGE);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_MAKE);
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
this.performForgeOperation(ForgeCommands.PACKAGE);
});
ipcRendererManager.on(IpcEvents.FIDDLE_MAKE, () => {
this.performForgeOperation(ForgeCommands.MAKE);
});
}
/**
* Actually run the fiddle.
*
* @returns {Promise<boolean>}
*/
public async run(): Promise<boolean> {
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { currentElectronVersion } = this.appState;
const { version, localPath } = currentElectronVersion;
if (this.appState.isClearingConsoleOnRun) {
this.appState.clearConsole();
}
this.appState.isConsoleShowing = true;
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
if (!dir) return false;
try {
await this.installModulesForEditor(values, dir);
} catch (error) {
console.error('Runner: Could not install modules', error);
fileManager.cleanup(dir);
return false;
}
const isReady = await getIsDownloaded(version, localPath);
if (!isReady) {
console.warn(`Runner: Binary ${version} not ready`);
let message = `Could not start fiddle: `;
message += `Electron ${version} not downloaded yet. `;
message += `Please wait for it to finish downloading `;
message += `before running the fiddle.`;
this.appState.pushOutput(message, { isNotPre: true });
fileManager.cleanup(dir);
return false;
}
this.execute(dir);
return true;
}
/**
* Stop a currently running Electron fiddle.
*/
public async stop() {
if (this.child) {
this.child.kill();
this.appState.isRunning = false;
}
}
/**
* Uses electron-forge to either package or make the current fiddle
*
* @param {ForgeCommands} operation
* @returns {Promise<boolean>}
* @memberof Runner
*/
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
const options = { includeDependencies: true, includeElectron: true };
const { dotfilesTransform } = await import('./transforms/dotfiles');
const { forgeTransform } = await import('./transforms/forge');
const { pushError, pushOutput } = this.appState;
const strings = operation === ForgeCommands.MAKE
? [ 'Creating installers for', 'Binary' ]
: [ 'Packaging', 'Installers' ];
this.appState.isConsoleShowing = true;
pushOutput(`📦 ${strings[0]} current Fiddle...`);
if (!(await getIsNpmInstalled())) {
let message = `Error: Could not find npm. Fiddle requires Node.js and npm `;
message += `to compile packages. Please visit https://nodejs.org to install `;
message += `Node.js and npm.`;
this.appState.pushOutput(message, { isNotPre: true });
return false;
}
// Save files to temp
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
if (!dir) return false;
// Files are now saved to temp, let's install Forge and dependencies
if (!(await this.npmInstall(dir))) return false;
// Cool, let's run "package"
try {
console.log(`Now creating ${strings[1].toLowerCase()}...`);
pushOutput(await npmRun({ dir }, operation));
pushOutput(`${strings[1]} successfully created.`, { isNotPre: true });
} catch (error) {
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
return false;
}
const { shell } = await import('electron');
shell.showItemInFolder(path.join(dir, 'out'));
return true;
}
/**
* Analyzes the editor's JavaScript contents for modules
* and installs them.
*
* @param {EditorValues} values
* @param {string} dir
* @returns {Promise<void>}
*/
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
const modules = await findModulesInEditors(values);
const { pushOutput } = this.appState;
if (modules && modules.length > 0) {
if (!(await getIsNpmInstalled())) {
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
message += `but we could not find npm. Fiddle requires Node.js and npm `;
message += `to support the installation of modules not included in `;
message += `Electron. Please visit https://nodejs.org to install Node.js `;
message += `and npm.`;
pushOutput(message, { isNotPre: true });
return;
}
pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
pushOutput(await installModules({ dir }, ...modules));
}
}
/**
* Execute Electron.
*
* @param {string} dir
* @param {string} version
* @returns {Promise<void>}
* @memberof Runner
*/
public async execute(dir: string): Promise<void> {
const { currentElectronVersion, pushOutput } = this.appState;
const { version, localPath } = currentElectronVersion;
const binaryPath = getElectronBinaryPath(version, localPath);
console.log(`Runner: Binary ${binaryPath} ready, launching`);
const env = { ...process.env };
if (this.appState.isEnablingElectronLogging) {
env.ELECTRON_ENABLE_LOGGING = 'true';
env.ELECTRON_DEBUG_NOTIFICATIONS = 'true';
env.ELECTRON_ENABLE_STACK_DUMPING = 'true';
} else {
delete env.ELECTRON_ENABLE_LOGGING;
delete env.ELECTRON_DEBUG_NOTIFICATIONS;
delete env.ELECTRON_ENABLE_STACK_DUMPING;
}
// Add user-specified cli flags if any have been set.
const options = [ dir, '--inspect' ].concat(this.appState.executionFlags);
this.child = spawn(binaryPath, options, { cwd: dir, env });
this.appState.isRunning = true;
pushOutput(`Electron v${version} started.`);
this.child.stdout!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.on('close', async (code) => {
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
: `.`;
pushOutput(`Electron exited${withCode}`);
this.appState.isRunning = false;
this.child = null;
// Clean older folders
await window.ElectronFiddle.app.fileManager.cleanup(dir);
await this.deleteUserData();
});
}
/**
* Save files to temp, logging to the Fiddle terminal while doing so
*
* @param {PackageJsonOptions} options
* @param {...Array<FileTransform>} transforms
* @returns {(Promise<string | null>)}
* @memberof Runner
*/
public async saveToTemp(
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string | null> {
const { fileManager } = window.ElectronFiddle.app;
const { pushOutput, pushError } = this.appState;
try {
pushOutput(`Saving files to temp directory...`);
const dir = await fileManager.saveToTemp(options, ...transforms);
pushOutput(`Saved files to ${dir}`);
return dir;
} catch (error) {
pushError('Failed to save files.', error);
}
return null;
}
/**
* Installs modules in a given directory (we're basically
* just running "npm install")
*
* @param {string} dir
* @returns
* @memberof Runner
*/
public async npmInstall(dir: string): Promise<boolean> {
try {
this.appState.pushOutput(`Now running "npm install..."`);
this.appState.pushOutput(await installModules({ dir }));
return true;
} catch (error) {
this.appState.pushError('Failed to run "npm install".', error);
}
return false;
}
/**
* Deletes the user data dir after a run.
*/
private async deleteUserData() {
if (this.appState.isKeepingUserDataDirs) {
console.log(`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`);
return;
}
const name = await this.appState.getName();
const appData = path.join(this.appState.appData, name);
console.log(`Cleanup: Deleting data dir ${appData}`);
await window.ElectronFiddle.app.fileManager.cleanup(appData);
}
}
+340 -91
View File
@@ -1,41 +1,58 @@
import { action, autorun, observable, when } from 'mobx';
import * as fsType from 'fs-extra';
import { action, autorun, computed, observable, when } from 'mobx';
import { MosaicNode } from 'react-mosaic-component';
import {
ElectronVersion,
ElectronVersionSource,
ElectronVersionState,
NpmVersion,
ALL_MOSAICS,
DocsDemoPage,
EditorId,
GenericDialogOptions,
GenericDialogType,
MosaicId,
OutputEntry,
OutputOptions,
WarningDialogTexts
RunnableVersion,
Version,
VersionSource,
VersionState
} from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { arrayToStringMap } from '../utils/array-to-stringmap';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
import { createMosaicArrangement, getVisibleMosaics } from '../utils/editors-mosaic-arrangement';
import { getName } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { BinaryManager } from './binary';
import { ContentNames, getContent, isContentUnchanged } from './content';
import { updateEditorTypeDefinitions } from './fetch-types';
import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
import { getDownloadedVersions, getDownloadingVersions, removeBinary, setupBinary } from './binary';
import { Bisector } from './bisect';
import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import { waitForEditorsToMount } from '../utils/editor-mounted';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import {
addLocalVersion,
ElectronReleaseChannel,
getDefaultVersion,
getElectronVersions,
getReleaseChannel,
getUpdatedElectronVersions,
saveLocalVersions
} from './versions';
const knownVersions = getElectronVersions();
const defaultVersion = localStorage.getItem('version')
|| normalizeVersion(knownVersions[0].version);
const defaultVersion = getDefaultVersion(knownVersions);
/**
* Editors exist outside of React's world. To make things *a lot*
* easier, we keep them around in a global object. Don't judge us,
* we're really only doing that for the editors.
*/
window.ElectronFiddle = {
window.ElectronFiddle = window.ElectronFiddle || {
editors: {
main: null,
renderer: null,
@@ -51,41 +68,63 @@ window.ElectronFiddle = {
* @class AppState
*/
export class AppState {
// Persisted settings
// -- Persisted settings ------------------
@observable public version: string = defaultVersion;
@observable public theme: string | null = localStorage.getItem('theme');
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
@observable public gitHubToken: string | null = localStorage.getItem('gitHubToken') || null;
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic', true);
@observable public versionsToShow: Array<ElectronReleaseChannel> =
this.retrieve('versionsToShow', true) as Array<ElectronReleaseChannel>
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
@observable public channelsToShow: Array<ElectronReleaseChannel> =
this.retrieve('channelsToShow') as Array<ElectronReleaseChannel>
|| [ElectronReleaseChannel.stable, ElectronReleaseChannel.beta];
@observable public statesToShow: Array<VersionState> =
this.retrieve('statesToShow') as Array<VersionState>
|| [VersionState.downloading, VersionState.ready, VersionState.unknown];
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
@observable public executionFlags: Array<string> =
this.retrieve('executionFlags') as Array<string> === null ?
[] : this.retrieve('executionFlags') as Array<string>;
@observable public binaryManager: BinaryManager = new BinaryManager();
// Various session-only state
// -- Various session-only state ------------------
@observable public gistId: string = '';
@observable public isMyGist: boolean = false;
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
@observable public versions: Record<string, RunnableVersion> = arrayToStringMap(knownVersions);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | null = null;
@observable public isUpdatingElectronVersions = false;
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public warningDialogLastResult: boolean | null = null;
@observable public localPath: string | undefined;
@observable public genericDialogOptions = { type: GenericDialogType.warning, label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public genericDialogLastResult: 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 Bisector: Bisector | undefined;
// Various "isShowing" settings
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean;
@observable public isUpdatingElectronVersions: boolean = false;
@observable public isQuitting: 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 isGenericDialogShowing: 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 isTourShowing: boolean = !localStorage.getItem('hasShownTour');
// -- Editor Values stored when we close the editor ------------------
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = DEFAULT_CLOSED_PANELS;
private outputBuffer: string = '';
private name: string;
public appData: string;
constructor() {
// Bind all actions
@@ -96,57 +135,141 @@ export class AppState {
this.setVersion = this.setVersion.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.clearConsole = this.clearConsole.bind(this);
this.toggleSettings = this.toggleSettings.bind(this);
this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
this.updateElectronVersions = this.updateElectronVersions.bind(this);
this.resetEditorLayout = this.resetEditorLayout.bind(this);
this.setIsQuitting = this.setIsQuitting.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.OPEN_SETTINGS);
ipcRendererManager.removeAllListeners(IpcEvents.SHOW_WELCOME_TOUR);
ipcRendererManager.removeAllListeners(IpcEvents.CLEAR_CONSOLE);
ipcRendererManager.removeAllListeners(IpcEvents.BISECT_COMMANDS_TOGGLE);
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
ipcRendererManager.on(IpcEvents.CLEAR_CONSOLE, this.clearConsole);
ipcRendererManager.on(IpcEvents.BISECT_COMMANDS_TOGGLE, this.toggleBisectCommands);
ipcRendererManager.on(IpcEvents.BEFORE_QUIT, this.setIsQuitting);
ipcRendererManager.once(IpcEvents.SET_APPDATA_DIR, (_event, dir) => { this.appData = dir; });
// Setup auto-runs
autorun(() => this.save('theme', this.theme));
autorun(() => this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun));
autorun(() => this.save('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('executionFlags', this.executionFlags));
autorun(() => this.save('version', this.version));
autorun(() => this.save('versionsToShow', this.versionsToShow));
autorun(() => this.save('channelsToShow', this.channelsToShow));
autorun(() => this.save('statesToShow', this.statesToShow));
autorun(() => {
if (this.isUnsaved) {
if (typeof this.isUnsaved === 'undefined') return;
if (!!this.isUnsaved) {
window.onbeforeunload = () => {
this.setWarningDialogTexts({
ipcRendererManager.send(IpcEvents.SHOW_INACTIVE);
this.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
ok: 'Quit'
});
this.isWarningDialogShowing = true;
this.isGenericDialogShowing = true;
// We'll wait until the warning dialog was closed
when(() => !this.isWarningDialogShowing).then(() => {
when(() => !this.isGenericDialogShowing).then(() => {
const quitConfirmed = this.genericDialogLastResult;
// The user confirmed, let's close for real.
if (this.warningDialogLastResult) {
if (quitConfirmed) {
window.onbeforeunload = null;
window.close();
}
// isQuitting checks if we're trying to quit the app
// or just close the window
if (this.isQuitting) {
ipcRendererManager.send(IpcEvents.CONFIRM_QUIT, quitConfirmed);
this.isQuitting = false;
}
});
// return value doesn't matter, we just want to cancel the event
return false;
};
} else {
window.onbeforeunload = null;
// set up editor listeners to verify if unsaved
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor = window.ElectronFiddle.editors[key];
const disposable = editor.onDidChangeModelContent(() => {
this.isUnsaved = true;
disposable.dispose();
});
});
}
});
// Update our known versions
this.updateElectronVersions();
// Make sure the console isn't all empty and sad
this.pushOutput('Console ready 🔬');
}
/**
* Returns the current RunnableVersion or the first
* one that can be found.
*/
@computed get currentElectronVersion(): RunnableVersion {
if (this.versions[this.version]) {
return this.versions[this.version];
} else {
return this.versions[defaultVersion];
}
}
/**
* Returns an array of Electron versions to show given the
* current settings for states and channels to display
*/
@computed get versionsToShow(): Array<RunnableVersion> {
return sortedElectronMap<RunnableVersion>(this.versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!this.channelsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!this.statesToShow.includes(item.state)) {
return false;
}
return true;
});
}
/**
* Update the Electron versions: First, fetch them from GitHub,
* then update their respective downloaded state.
*
* Fails silently.
*/
@action public async updateElectronVersions() {
this.isUpdatingElectronVersions = true;
@@ -156,7 +279,7 @@ export class AppState {
this.versions = arrayToStringMap(versions);
await this.updateDownloadedVersionState();
} catch (error) {
console.warn(`State: Could not update Electron versions`);
console.warn(`State: Could not update Electron versions`, error);
}
this.isUpdatingElectronVersions = false;
@@ -174,22 +297,41 @@ export class AppState {
this.isConsoleShowing = !this.isConsoleShowing;
}
@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() {
this.isWarningDialogShowing = !this.isWarningDialogShowing;
@action public toggleGenericDialog() {
this.isGenericDialogShowing = !this.isGenericDialogShowing;
if (this.isWarningDialogShowing) {
this.warningDialogLastResult = null;
if (this.isGenericDialogShowing) {
this.genericDialogLastResult = null;
}
}
@action public toggleBisectDialog() {
this.isBisectDialogShowing = !this.isBisectDialogShowing;
}
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
@@ -200,6 +342,10 @@ export class AppState {
this.resetView({ isSettingsShowing: !this.isSettingsShowing });
}
@action public setIsQuitting() {
this.isQuitting = true;
}
@action public disableTour() {
this.resetView({ isTourShowing: false });
localStorage.setItem('hasShownTour', 'true');
@@ -215,27 +361,28 @@ export class AppState {
window.ElectronFiddle.app.setupTheme();
}
@action public setWarningDialogTexts(input: WarningDialogTexts) {
this.warningDialogTexts = {
@action public setGenericDialogOptions(opts: GenericDialogOptions) {
this.genericDialogOptions = {
type: GenericDialogType.warning,
ok: 'Okay',
cancel: 'Cancel',
...input
...opts
};
}
@action public async addLocalVersion(input: NpmVersion) {
@action public addLocalVersion(input: Version) {
addLocalVersion(input);
this.versions = arrayToStringMap(getElectronVersions());
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];
@@ -252,7 +399,7 @@ export class AppState {
const updatedVersions = { ...this.versions };
// Actually remove
if (release && release.source === ElectronVersionSource.local) {
if (release && release.source === VersionSource.local) {
delete updatedVersions[version];
const versionsAsArray = Object
@@ -261,49 +408,48 @@ export class AppState {
saveLocalVersions(versionsAsArray);
} else {
await this.binaryManager.remove(version);
updatedVersions[version].state = ElectronVersionState.unknown;
await removeBinary(version);
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}`);
const release = this.versions[version] || { state: '', source: '' };
const isLocal = release.source === ElectronVersionSource.local;
const isLocal = release.source === VersionSource.local;
const isReady = release.state === 'ready';
// Fetch new binaries, maybe?
if (!isLocal && !isReady) {
console.log(`State: Instructing BinaryManager to fetch v${version}`);
console.log(`State: Fetching v${version}`);
const updatedVersions = { ...this.versions };
updatedVersions[version] = updatedVersions[version] || {};
updatedVersions[version].state = ElectronVersionState.downloading;
this.versions = updatedVersions;
await this.binaryManager.setup(version);
await setupBinary(this, version);
this.updateDownloadedVersionState();
} else {
console.log(`State: Version ${version} already downloaded, doing nothing.`);
}
}
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* 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);
@@ -319,34 +465,64 @@ 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 === VersionSource.local) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typePath = getLocalTypePathForVersion(versionObject);
console.info(`TypeDefs: Watching file for local version ${version} at path ${typePath}`);
try {
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
await updateEditorTypeDefinitions(versionObject);
});
} catch (err) {
console.info('TypeDefs: Unable to start watching.');
}
} 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 };
console.log(`State: Updating version state`);
(downloadedVersions || []).forEach((version) => {
// Keep state of currently downloading binaries first
const downloadingVersions = getDownloadingVersions(this);
(downloadingVersions || []).forEach((version) => {
if (updatedVersions[version]) {
updatedVersions[version].state = ElectronVersionState.ready;
updatedVersions[version].state = VersionState.downloading;
}
});
const downloadedVersions = await getDownloadedVersions();
(downloadedVersions || []).forEach((version) => {
if (updatedVersions[version]) {
updatedVersions[version].state = VersionState.ready;
}
});
console.log(`State: Updated version state`, updatedVersions);
this.versions = updatedVersions;
}
@@ -374,12 +550,11 @@ 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;
const parts = strData.split('\r\n');
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
const part = parts[partIndex];
if (partIndex === parts.length - 1) {
@@ -415,6 +590,84 @@ 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 async 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;
// if we have backup, remove active editor
delete window.ElectronFiddle.editors[id];
}
// 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;
// after the mosaicArrangement loads, we want to wait for the Mosaic editors to
// mount to ensure that we can load content into the editors as soon as they're
// declared visible.
await waitForEditorsToMount(visible);
}
/**
* 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 editor view to default layout
*
*
*/
@action public resetEditorLayout() {
this.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
}
/**
* Resets the view, optionally with certain view flags enabled.
*
@@ -427,6 +680,7 @@ export class AppState {
this.isTourShowing = false;
this.isConsoleShowing = false;
this.isAddVersionDialogShowing = false;
this.isThemeDialogShowing = false;
if (additionalOptions) {
for (const key in additionalOptions) {
@@ -479,17 +733,12 @@ export class AppState {
*
* @template T
* @param {string} key
* @param {boolean} parse
* @returns {(T | string | null)}
*/
private retrieve<T>(key: string, parse: boolean): T | string | null {
private retrieve<T>(key: string): T | string | null {
const value = localStorage.getItem(key);
if (parse) {
return JSON.parse(value || 'null') as T;
}
return value;
return JSON.parse(value || 'null') as T;
}
}
+16 -7
View File
@@ -1,13 +1,12 @@
import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../constants';
import { EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_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,8 @@ 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),
css: await getFile(STYLES_CSS_NAME)
};
}
+2 -2
View File
@@ -2,8 +2,8 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { CONFIG_PATH } from '../constants';
import { fancyImport } from '../utils/import';
import { CONFIG_PATH } from './constants';
import { defaultDark, defaultLight, DefaultThemes, FiddleTheme, LoadedFiddleTheme } from './themes-defaults';
export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
@@ -20,7 +20,7 @@ export async function activateTheme(
const _monaco = monaco || window.ElectronFiddle.app.monaco;
const _theme = theme || await getTheme(name);
if (!_monaco) return;
if (!_monaco || !_monaco.editor) return;
_monaco.editor.defineTheme('main', _theme.editor as any);
_monaco.editor.setTheme('main');
+283
View File
@@ -0,0 +1,283 @@
import {
remote,
TouchBarButtonConstructorOptions,
TouchBarScrubberConstructorOptions
} from 'electron';
import { autorun } from 'mobx';
import { RunnableVersion, VersionState } from '../interfaces';
import { getNiceGreeting } from '../utils/nice-greeting';
import { sortedElectronMap } from '../utils/sorted-electron-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<RunnableVersion>} { state }
* @returns {string}
*/
export function getItemIcon(
{ state }: Partial<RunnableVersion> = { 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];
}
}
+6 -6
View File
@@ -1,5 +1,5 @@
import { PACKAGE_NAME } from '../../constants';
import { Files } from '../../interfaces';
import { PACKAGE_NAME } from '../../shared-constants';
/**
* This transform turns the files into an electron-forge
@@ -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 || {};
+85 -47
View File
@@ -1,4 +1,6 @@
import { ElectronVersion, ElectronVersionSource, ElectronVersionState, NpmVersion } from '../interfaces';
import semver from 'semver';
import { RunnableVersion, Version, VersionSource, VersionState } from '../interfaces';
import { normalizeVersion } from '../utils/normalize-version';
export const enum ElectronReleaseChannel {
stable = 'Stable',
@@ -7,17 +9,50 @@ export const enum ElectronReleaseChannel {
unsupported = 'Unsupported'
}
/**
* Returns a sensible default version string.
*
* @param {Array<RunnableVersion>} knownVersions
* @returns {string}
*/
export function getDefaultVersion(
knownVersions: Array<RunnableVersion> = []
): string {
const ls = localStorage.getItem('version');
if (ls && knownVersions && knownVersions.find(({ version }) => version === ls)) {
return ls;
}
// Self-heal: Version not formated correctly
const normalized = ls && normalizeVersion(ls);
if (normalized) {
if (knownVersions && knownVersions.find(({ version }) => version === normalized)) {
return normalized;
}
}
// Alright, the first version?
const last = knownVersions && knownVersions[knownVersions.length - 1];
if (last) {
return last.version;
}
// Report error
throw new Error('Corrupted version data');
}
/**
* Return the release channel for a given input
* version.
*
* @param {NpmVersion} input
* @param {Version | string} input
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: NpmVersion
input: Version | string
): ElectronReleaseChannel {
const tag = input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
@@ -44,17 +79,17 @@ export const enum VersionKeys {
* Retrieve Electron versions from localStorage.
*
* @param {VersionKeys} key
* @param {() => Array<NpmVersion>} fallbackMethod
* @returns {Array<NpmVersion>}
* @param {() => Array<Version>} fallbackMethod
* @returns {Array<Version>}
*/
function getVersions(
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
): Array<NpmVersion> {
key: VersionKeys, fallbackMethod: () => Array<Version>
): Array<Version> {
const fromLs = window.localStorage.getItem(key);
if (fromLs) {
try {
let result: Array<NpmVersion> = JSON.parse(fromLs);
let result: Array<Version> = JSON.parse(fromLs);
if (!isExpectedFormat(result)) {
// Known versions can just be downloaded again.
@@ -80,9 +115,9 @@ function getVersions(
* Save an array of GitHubVersions to localStorage.
*
* @param {VersionKeys} key
* @param {Array<NpmVersion} versions
* @param {Array<Version} versions
*/
function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
function saveVersions(key: VersionKeys, versions: Array<Version>) {
const stringified = JSON.stringify(versions);
window.localStorage.setItem(key, stringified);
}
@@ -90,35 +125,35 @@ function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
/**
* Return both known as well as local versions.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getElectronVersions(): Array<ElectronVersion> {
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
export function getElectronVersions(): Array<RunnableVersion> {
const known: Array<RunnableVersion> = getKnownVersions().map((version) => {
return {
...version,
source: ElectronVersionSource.remote,
state: ElectronVersionState.unknown
source: VersionSource.remote,
state: VersionState.unknown
};
});
const local: Array<ElectronVersion> = getLocalVersions().map((version) => {
const local: Array<RunnableVersion> = getLocalVersions().map((version) => {
return {
...version,
source: ElectronVersionSource.local,
state: ElectronVersionState.ready
source: VersionSource.local,
state: VersionState.ready
};
});
return [ ...known, ...local ];
return [...known, ...local];
}
/**
* Add a version to the local versions
*
* @param {NpmVersion} input
* @returns {Array<NpmVersion>}
* @param {Version} input
* @returns {Array<Version>}
*/
export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
export function addLocalVersion(input: Version): Array<Version> {
const versions = getLocalVersions();
if (!versions.find((v) => v.localPath === input.localPath)) {
@@ -133,9 +168,9 @@ export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
/**
* Retrieves local Electron versions, configured by the user.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getLocalVersions(): Array<NpmVersion> {
export function getLocalVersions(): Array<Version> {
const versions = getVersions(VersionKeys.local, () => []);
return versions;
@@ -144,12 +179,12 @@ export function getLocalVersions(): Array<NpmVersion> {
/**
* Saves local versions to localStorage.
*
* @param {Array<NpmVersion>} versions
* @param {Array<Version>} versions
*/
export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>) {
export function saveLocalVersions(versions: Array<Version | RunnableVersion>) {
const filteredVersions = versions.filter((v) => {
if (isElectronVersion(v)) {
return v.source === ElectronVersionSource.local;
return v.source === VersionSource.local;
}
return true;
@@ -162,18 +197,18 @@ export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>)
* Retrieves our best guess regarding the latest Electron versions. Tries to
* fetch them from localStorage, then from a static releases.json file.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getKnownVersions(): Array<NpmVersion> {
export function getKnownVersions(): Array<Version> {
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
}
/**
* Saves known versions to localStorage.
*
* @param {Array<NpmVersion>} versions
* @param {Array<Version>} versions
*/
export function saveKnownVersions(versions: Array<NpmVersion>) {
export function saveKnownVersions(versions: Array<Version>) {
return saveVersions(VersionKeys.known, versions);
}
@@ -182,10 +217,10 @@ export function saveKnownVersions(versions: Array<NpmVersion>) {
* saved after.
*
* @export
* @returns {Promise<Array<ElectronVersion>>}
* @returns {Promise<Array<RunnableVersion>>}
*/
export async function getUpdatedElectronVersions(
): Promise<Array<ElectronVersion>> {
): Promise<Array<RunnableVersion>> {
try {
await fetchVersions();
} catch (error) {
@@ -198,18 +233,21 @@ export async function getUpdatedElectronVersions(
/**
* Fetch the latest known versions directly from npm.
*
* @returns {Promise<Array<NpmVersion>>}
* @returns {Promise<Array<Version>>}
*/
export async function fetchVersions() {
const response = await window.fetch(`https://registry.npmjs.org/electron`);
const response = await window.fetch('https://unpkg.com/electron-releases/lite.json');
const data = await response.json();
const versions: Record<string, any> = data.versions;
const output: Array<NpmVersion> = Object
.keys(versions)
.map((version) => ({ version }));
// pre-0.24.0 versions were technically 'atom-shell' and cannot
// be downloaded with @electron/get
const MIN_DOWNLOAD_VERSION = '0.24.0';
if (output && output.length > 0 && isExpectedFormat(output)) {
const output = data
.map(({ version }: any) => ({ version }))
.filter(({ version }: any) => semver.gte(version, MIN_DOWNLOAD_VERSION));
if (output?.length > 0 && isExpectedFormat(output)) {
console.log(`Fetched new Electron versions (Count: ${output.length})`);
saveKnownVersions(output);
}
@@ -231,9 +269,9 @@ export function isExpectedFormat(input: Array<any>): boolean {
* Migrates old versions, if necessary
*
* @param {Array<any>} input
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
export function migrateVersions(input: Array<any> = []): Array<Version> {
return input
.filter((item) => !!item)
.map((item) => {
@@ -247,11 +285,11 @@ export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
localPath: url
};
})
.filter((item) => !!item) as Array<NpmVersion>;
.filter((item) => !!item) as Array<Version>;
}
export function isElectronVersion(
input: NpmVersion | ElectronVersion
): input is ElectronVersion {
return (input as ElectronVersion).source !== undefined;
input: Version | RunnableVersion
): input is RunnableVersion {
return (input as RunnableVersion).source !== undefined;
}

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