├── .all-contributorsrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── appveyor.yml ├── circle.yml ├── contribution.json ├── keymaps └── project-viewer.json ├── menus └── project-viewer.json ├── package-lock.json ├── package.json ├── spec ├── api-spec.js ├── colours-spec.js ├── common-spec.js ├── database-spec.js ├── db-spec.js ├── dom-builder-spec.js ├── group-model-spec.js ├── group-view-spec.js ├── main-spec.js └── project-model-spec.js ├── src ├── api.js ├── colours.js ├── common.js ├── config.js ├── constants.js ├── database.js ├── db.js ├── dom-builder.js ├── editor-view.js ├── group-view.js ├── json │ ├── devicons.json │ ├── octicons.json │ └── release-notes.json ├── main-view.js ├── main.js ├── map.js ├── model.js ├── packages.js ├── project-view.js ├── projects-list-view.js ├── projects-list.js ├── status-bar.js └── workers │ └── github.js └── styles ├── project-viewer.less └── pv-variables.less /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "atom-project-viewer", 3 | "projectOwner": "jccguimaraes", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "jccguimaraes", 12 | "name": "João Guimarães", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/14871650?v=3", 14 | "profile": "https://github.com/jccguimaraes", 15 | "contributions": [ 16 | "question", 17 | "bug", 18 | "code", 19 | "design", 20 | "doc", 21 | "review" 22 | ] 23 | }, 24 | { 25 | "login": "Hammster", 26 | "name": "Hans Koch", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/1093709?v=3", 28 | "profile": "https://github.com/Hammster", 29 | "contributions": [ 30 | "code" 31 | ] 32 | }, 33 | { 34 | "login": "DamnedScholar", 35 | "name": "Holland Wilson", 36 | "avatar_url": "https://avatars.githubusercontent.com/u/4084322?v=3", 37 | "profile": "https://github.com/DamnedScholar", 38 | "contributions": [ 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "amilor", 44 | "name": "Roman Huba", 45 | "avatar_url": "https://avatars.githubusercontent.com/u/7261682?v=3", 46 | "profile": "https://github.com/amilor", 47 | "contributions": [ 48 | "code" 49 | ] 50 | }, 51 | { 52 | "login": "lneveu", 53 | "name": "Loann Neveu", 54 | "avatar_url": "https://avatars.githubusercontent.com/u/10619585?v=3", 55 | "profile": "https://github.com/lneveu", 56 | "contributions": [ 57 | "code", 58 | "bug" 59 | ] 60 | }, 61 | { 62 | "login": "bitkris-dev", 63 | "name": "Kristian Barrese", 64 | "avatar_url": "https://avatars0.githubusercontent.com/u/12634286?v=3&s=460", 65 | "profile": "https://github.com/bitkris-dev", 66 | "contributions": [ 67 | "design", 68 | "bug" 69 | ] 70 | }, 71 | { 72 | "login": "girlandhercode", 73 | "name": "Nicole", 74 | "avatar_url": "https://avatars.githubusercontent.com/u/2183606?v=3", 75 | "profile": "https://github.com/girlandhercode", 76 | "contributions": [ 77 | "bug" 78 | ] 79 | }, 80 | { 81 | "login": "colorful-tones", 82 | "name": "Damon Cook", 83 | "avatar_url": "https://avatars.githubusercontent.com/u/405912?v=3", 84 | "profile": "http://www.damonacook.com", 85 | "contributions": [ 86 | "bug" 87 | ] 88 | }, 89 | { 90 | "login": "zhudock", 91 | "name": "Zach Hudock", 92 | "avatar_url": "https://avatars.githubusercontent.com/u/12414689?v=3", 93 | "profile": "https://github.com/zhudock", 94 | "contributions": [ 95 | "bug" 96 | ] 97 | }, 98 | { 99 | "login": "CKLFP", 100 | "name": "Filip Paço", 101 | "avatar_url": "https://avatars.githubusercontent.com/u/5192692?v=3", 102 | "profile": "https://github.com/CKLFP", 103 | "contributions": [ 104 | "bug" 105 | ] 106 | }, 107 | { 108 | "login": "stephen-last", 109 | "name": "Stephen Last", 110 | "avatar_url": "https://avatars2.githubusercontent.com/u/16349203?v=3", 111 | "profile": "https://github.com/stephen-last", 112 | "contributions": [ 113 | "bug" 114 | ] 115 | }, 116 | { 117 | "login": "GreenGremlin", 118 | "name": "Jonathan", 119 | "avatar_url": "https://avatars2.githubusercontent.com/u/647452?v=3", 120 | "profile": "https://github.com/GreenGremlin", 121 | "contributions": [ 122 | "bug" 123 | ] 124 | }, 125 | { 126 | "login": "skratchdot", 127 | "name": "◬", 128 | "avatar_url": "https://avatars2.githubusercontent.com/u/434470?v=3", 129 | "profile": "http://skratchdot.com/", 130 | "contributions": [ 131 | "bug" 132 | ] 133 | }, 134 | { 135 | "login": "erikgeiser", 136 | "name": "Erik G.", 137 | "avatar_url": "https://avatars0.githubusercontent.com/u/14264874?v=3", 138 | "profile": "https://github.com/erikgeiser", 139 | "contributions": [ 140 | "code" 141 | ] 142 | }, 143 | { 144 | "login": "rgawenda", 145 | "name": "netizen", 146 | "avatar_url": "https://avatars2.githubusercontent.com/u/3426685?v=4", 147 | "profile": "https://github.com/rgawenda", 148 | "contributions": [ 149 | "code" 150 | ] 151 | }, 152 | { 153 | "login": "mdeuerlein", 154 | "name": "Markus M. Deuerlein", 155 | "avatar_url": "https://avatars0.githubusercontent.com/u/14030524?v=4", 156 | "profile": "https://entidia.de", 157 | "contributions": [ 158 | "bug", 159 | "code" 160 | ] 161 | }, 162 | { 163 | "login": "audrummer15", 164 | "name": "Adam Brown", 165 | "avatar_url": "https://avatars2.githubusercontent.com/u/1392689?v=4", 166 | "profile": "https://github.com/audrummer15", 167 | "contributions": [ 168 | "code" 169 | ] 170 | } 171 | ] 172 | } 173 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = auto 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue 2 | Description of the **issue**. 3 | 4 | ## Environment 5 | - **OS**: ...; 6 | - **Atom**: ...; 7 | - Any other particularity; 8 | 9 | ## Steps 10 | Please try to describe **step by step** how to get to the issue 11 | 12 | > and if possible provide a screenshot. :+1: 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | Description of the **pull request**. 3 | 4 | ## Reason 5 | Please try to describe **the reason** for this. Thanks! 6 | 7 | > and if possible provide a screenshot. :+1: 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.2.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | - ATOM_CHANNEL=beta 12 | 13 | os: 14 | - linux 15 | - osx 16 | 17 | dist: trusty 18 | 19 | ### Generic setup follows ### 20 | script: 21 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 22 | - chmod u+x build-package.sh 23 | - ./build-package.sh 24 | 25 | notifications: 26 | email: 27 | on_success: never 28 | on_failure: change 29 | 30 | branches: 31 | only: 32 | - master 33 | 34 | git: 35 | depth: 10 36 | 37 | sudo: false 38 | 39 | addons: 40 | apt: 41 | sources: 42 | - ubuntu-toolchain-r-test 43 | packages: 44 | - g++-6 45 | - fakeroot 46 | - git 47 | - libsecret-1-dev 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | There are two main ways to contribute to this package. Either open an issue on the [*github*]() page or **fork** the package and send a **Pull Request**. 6 | 7 | ## Opening Issues 8 | 9 | For helping me maintain this package, please follow the next guidelines when opening issues on [*github*](). 10 | 11 | > **NOTE** Please provide screenshots if possible! 12 | 13 | #### Feature Requests 14 | Please start an issue with the following title `(feature): description`. 15 | 16 | #### Issues / Defects 17 | Please start an issue with the following title `(issue): description`. 18 | 19 | #### Performance / Refactor 20 | Please start an issue with the following title `(perf): description`. 21 | 22 | ## Making a Pull Request 23 | 24 | Just **fork** the project, enjoy coding and send a **Pull Request** to me. Will haply inspect and accept it. 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 João Guimarães 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | [![Join the chat at https://gitter.im/jccguimaraes/atom-project-viewer](http://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg?style=flat-square)](https://gitter.im/jccguimaraes/atom-project-viewer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors) 5 | 6 | [![atom version](https://img.shields.io/badge/atom-1.18.0-orange.svg?style=flat-square)](https://atom.io/packages/project-viewer/) 7 | [![apm version](https://img.shields.io/apm/v/project-viewer.svg?style=flat-square)](https://atom.io/packages/project-viewer/) 8 | [![apm downloads](https://img.shields.io/apm/dm/project-viewer.svg?style=flat-square)](https://atom.io/packages/project-viewer/) 9 | 10 | [![Travis CI CI](https://travis-ci.org/jccguimaraes/atom-project-viewer.svg?branch=master)](https://travis-ci.org/jccguimaraes/atom-project-viewer) 11 | [![AppVeyor CI](https://ci.appveyor.com/api/projects/status/2t91cemmpf635p2e?svg=true 12 | )](https://ci.appveyor.com/project/jccguimaraes/atom-project-viewer) 13 | [![CircleCI CI](https://circleci.com/gh/jccguimaraes/atom-project-viewer/tree/master.svg?style=shield&circle-token=c2215983920e08d193a80b5775760792c5d2e883 14 | )](https://circleci.com/gh/jccguimaraes/atom-project-viewer) 15 | 16 | ## Table Of Contents 17 | 18 | * [Introduction](#introduction) 19 | * [Installation](#installation) 20 | * [Features](#features) 21 | * [Shortcuts](#shortcuts) 22 | * [Settings](#settings) 23 | * [Local File manipulation](#local-file-manipulation) 24 | * [Group Schema](#group-schema) 25 | * [Project Schema](#project-schema) 26 | * [Contributors](#contributors) 27 | * [Contacts](#contacts) 28 | * [A Special Thank You!](#a-special-thank-you) 29 | 30 | ## Introduction 31 | 32 | This is a package built for and by the Atom community. For contribution read [below](#contributors). 33 | 34 | This package has grown so much over the last year that I felt the need to make it more stable and community friendly. And this required a more deep refactor with lots of new ideas and improvements, also huge amount of :heart: and :sweat_drops:. 35 | 36 | So here it is! **Enjoy and contribute!** :earth_africa: 37 | 38 | > Please keep in mind that after **Atom `1.17.0`** some functionalities changed, and implementations of this package are still being tested for stability. 39 | 40 | ## Installation 41 | 42 | In a terminal / command line write the following line `apm install project-viewer`. 43 | 44 | Or just find the package by accessing the menu **Atom → Preferences... → Install** and search for ***project-viewer***. 45 | 46 | ## Features 47 | 48 | - Group nesting; 49 | - > Infinite nesting of `groups` which can contain also `projects`; 50 | - > `projects` can be at any level. 51 | - Sidebar Left / Right (first or last) position; 52 | - Auto hide sidebar with hover behavior; 53 | - Resizable panel; 54 | - > *Double click* to default width; 55 | - Hide header for more space; 56 | - > This is available through a config option, default is *not autohide*. 57 | - Focus toggle; 58 | - > Toggling focus will switch between current active element and the panel. 59 | - `SelectListView` integration; 60 | - > Only shows `projects`. 61 | - Traverse and select `projects` with `up` and `down` keys; 62 | - Toggle collapse / expand of `groups` with `left` and `right` keys; 63 | - `status-bar` with the `project`'s' *breadcrumb* path; 64 | - Drag & Drop `groups` and `projects`; 65 | - Drag and drop a `group` or `project` into a `group` will add it as a child; 66 | - Drag and drop a `group` or `project` into an `project` will add it as sibling of the dropped item; 67 | - Drag and drop a `group` or `project` into a clear space in the panel will add it as a root child; 68 | - Order dragged `group` / `project` accordingly with dropped `group` sorting. 69 | - Open the local database file for direct editing; 70 | - Old database schemas conversion tools; 71 | - Backup services (**GitHub's *private* gist**); 72 | - Editor for `groups` / `projects` creation and update; 73 | - Create, update and remove `group` or `project`; 74 | - Automatic set it's name according to first path base name added; 75 | - Batch operation on a `project` creation; 76 | - > Ability to create individual `projects` when more than one path is provided; 77 | - > Each project will automatically have it's name set to it's path base name. 78 | - Filtering icons; 79 | - List of icons in editor as *only icons* or *icon and description*; 80 | - > This is available through a config option, default is *icon and description*. 81 | - Sort children `groups` / `projects`. 82 | - > Sorting root `groups` / `projects` is done through a config option. 83 | - Context menu for delete, update and create new `group` or `project`; 84 | - > Create option is only available in `groups` or the `root`. 85 | - Show the given path in a file manager. (in `finder` or `explorer`'s alike'); 86 | - Empty `groups` and / or `projects` list message; 87 | - Custom colors for `groups` and `projects`; 88 | - Custom colors for main title, for hovering on a `project` and for selected `project`; 89 | - Option to open a `project` in a new window or vice versa; 90 | - > This is available through a config option which will switch between what is the primary option, defaults to open in *same window*; 91 | - > Context menu switching also available. 92 | - Elevate current opened folders in `tree-view` to a `project`; 93 | - `Add Project Folder` and `Remove Project Folder` will update current selected project as well; 94 | - Keep context when switching from `projects`. 95 | - > This is available through a config option, default is *switch contexts*. 96 | 97 | ## Shortcuts 98 | 99 | - `shift-ctrl-alt-c` toggles sidebar autohide; 100 | - `shift-ctrl-alt-v` toggles sidebar visibility; 101 | - `shift-ctrl-alt-n` open the editor tab; 102 | - `shift-ctrl-alt-m` toggle focus from active panel and the sidebar; 103 | - `shift-ctrl-alt-l` toggle the select list modal; 104 | 105 | ## Settings 106 | 107 | Settings | Type | Description | Default 108 | ---------|------|-------------|-------- 109 | `visibilityOption` | `String` | Define what would be the default action for **project-viewer** visibility on startup. | `Display on startup` 110 | `visibilityActive` | `Boolean` | Relative to the interaction option selected above. | `true` 111 | `panelPosition` | `String` | Position the panel to the left or right of the main pane. | `Right` 112 | `autoHide` | `Boolean` | Panel has auto hide with hover behavior. | `false` 113 | `hideHeader` | `Boolean` | You can have more space for the list by hiding the header. | `false` 114 | `keepContext` | `Boolean` | When switching from items, if set to `true`, will keep current context. Also will not save contexts between switching. | `false` 115 | `openNewWindow` | `Boolean` | Always open items in a new window. | `false` 116 | `statusBar` | `Boolean` | Will show the breadcrumb to the current opened project in the `status-bar`. | `false` 117 | `customWidth` | `Integer` | Define a custom width for the panel.
*double clicking* on the resizer will reset the width | 200 118 | `customHotZone` | `Integer` | Cursor movement within this width will make a hidden panel appear | 20 119 | `rootSortBy` | `Array` | Sets the root sort by. | `position` 120 | `onlyIcons` | `Boolean` | Will show only the icons in the icon\'s list | `true` 121 | `customPalette` | `String` | Custom palette to use on editor | `#F1E4E8, #F7B05B, #595959, #CD5334, #EDB88B, #23282E, #263655, #F75468, #FF808F, #FFDB80, #292E1E, #248232, #2BA84A, #D8DAD3, #FCFFFC, #8EA604, #F5BB00, #EC9F05, #FF5722, #BF3100` 122 | `customSelectedColor` | `String` | Set custom selected project color | `''` 123 | `customHoverColor` | `String` | Set custom hover project color | `''` 124 | `customTitleColor` | `String` | Set custom main title color | `''` 125 | `packagesReload` | `String` | List of packages to reload | `status-bar, linter, linter-ui-default` 126 | `disclaimer` | `Object` | Show release notes on startup | `true` 127 | 128 | > Keep in mind that this package uses Atom's Storage to save all groups and projects. It is wise to save it to the cloud (ex: you can import and export a private Gist through this package!). 129 | 130 | ## Local File manipulation 131 | 132 | Change it at your own risk! :speak_no_evil: 133 | 134 | ### Group Schema 135 | 136 | Parameter | Type | Description | Default | Required 137 | ----------|------|-------------|---------|--------- 138 | `type` | `String` | The type of the model | `group` | `true` 139 | `name` | `String` | The name of the project | In theory... any string / emoji | `true` 140 | `sortBy` | `String` | Sorting of the nested `groups` and `projects` | Possible options are `position`, `reserve-position`, `alphabetically` and `reverse-alphabetically` | `true` 141 | `icon` | `String` | Custom icon `octicons` or `devicons` | `''` | `false` 142 | `color` | `String` | Custom color | `''` | `false` 143 | `expanded` | `Boolean` | `group` is collapsed or expanded | `false` | `true` 144 | `list` | `Array` | An array of models (`group` or `project` | `[]` | `true` 145 | 146 | ### Project Schema 147 | 148 | Parameter | Type | Description | Default | Required 149 | ----------|------|-------------|---------|--------- 150 | `type` | `String` | The type of the model | `project` | `true` 151 | `name` | `String` | The name of the project | In theory... any strj g / emoji | `true` 152 | `icon` | `String` | Custom icon `octicons` or `devicons` | `''` | `false` 153 | `color` | `String` | Custom color | `''` | `false` 154 | `devMode` | `Boolean` | *Not working for now* | `false` | `false` 155 | `config` | `Object` | *Not working for now* | `{}` | `false` 156 | `paths` | `Array` | An array of the root files beloging to the project | `[]` | `true` 157 | 158 | ## Contributors 159 | 160 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 161 | 162 | 163 | | [
João Guimarães](https://github.com/jccguimaraes)
💬 [🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Ajccguimaraes) [💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=jccguimaraes) 🎨 [📖](https://github.com/jccguimaraes/atom-project-viewer/commits?author=jccguimaraes) 👀 | [
Hans Koch](https://github.com/Hammster)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=Hammster) | [
Holland Wilson](https://github.com/DamnedScholar)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=DamnedScholar) | [
Roman Huba](https://github.com/amilor)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=amilor) | [
Loann Neveu](https://github.com/lneveu)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=lneveu) [🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Alneveu) | [
Kristian Barrese](https://github.com/bitkris-dev)
🎨 [🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Abitkris-dev) | [
Nicole](https://github.com/girlandhercode)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Agirlandhercode) | 164 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 165 | | [
Damon Cook](http://www.damonacook.com)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Acolorful-tones) | [
Zach Hudock](https://github.com/zhudock)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Azhudock) | [
Filip Paço](https://github.com/CKLFP)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3ACKLFP) | [
Stephen Last](https://github.com/stephen-last)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Astephen-last) | [
Jonathan](https://github.com/GreenGremlin)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3AGreenGremlin) | [
](http://skratchdot.com/)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Askratchdot) | [
Erik G.](https://github.com/erikgeiser)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=erikgeiser) | 166 | | [
netizen](https://github.com/rgawenda)
[💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=rgawenda) | [
Markus M. Deuerlein](https://entidia.de)
[🐛](https://github.com/jccguimaraes/atom-project-viewer/issues?q=author%3Amdeuerlein) [💻](https://github.com/jccguimaraes/atom-project-viewer/commits?author=mdeuerlein) | 167 | 168 | 169 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 170 | 171 | > If you feel you were left out, just shout! 172 | 173 | ## Contacts 174 | 175 | You can follow me on [Twitter](https://twitter.com/jccguimaraes) 176 | 177 | ## A Special Thank You! 178 | 179 | I thank you all for giving such great feedback! :beers: & :bear: for everyone. 180 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | environment: 3 | APM_TEST_PACKAGES: 4 | ATOM_LINT_WITH_BUNDLED_NODE: "true" 5 | 6 | matrix: 7 | - ATOM_CHANNEL: stable 8 | - ATOM_CHANNEL: beta 9 | 10 | ### Generic setup follows ### 11 | build_script: 12 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 13 | 14 | branches: 15 | only: 16 | - master 17 | 18 | version: "{build}" 19 | platform: x64 20 | clone_depth: 10 21 | skip_tags: true 22 | test: off 23 | deploy: off 24 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | ATOM_LINT_WITH_BUNDLED_NODE: "true" 4 | APM_TEST_PACKAGES: "" 5 | 6 | dependencies: 7 | override: 8 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 9 | - chmod u+x build-package.sh 10 | 11 | test: 12 | override: 13 | - ./build-package.sh 14 | -------------------------------------------------------------------------------- /contribution.json: -------------------------------------------------------------------------------- 1 | // https://gitmagic.io/rules 2 | { 3 | "commit": { 4 | "subject_cannot_be_empty": true, 5 | "subject_must_be_longer_than": 4, 6 | "subject_must_be_shorter_than": 101, 7 | "subject_lines_must_be_shorter_than": 51, 8 | "subject_must_be_single_line": true, 9 | "subject_must_be_in_tense": "imperative", 10 | "subject_must_start_with_case": "lower", 11 | "subject_must_not_end_with_dot": true, 12 | 13 | "body_lines_must_be_shorter_than": 73 14 | }, 15 | "pull_request": { 16 | "subject_cannot_be_empty": true, 17 | "subject_must_be_longer_than": 4, 18 | "subject_must_be_shorter_than": 101, 19 | "subject_must_be_in_tense": "imperative", 20 | "subject_must_start_with_case": "upper", 21 | "subject_must_not_end_with_dot": true, 22 | 23 | "body_cannot_be_empty": true, 24 | "body_must_include_verification_steps": true, 25 | "body_must_include_screenshot": ["html", "css"] 26 | }, 27 | "issue": { 28 | "subject_cannot_be_empty": true, 29 | "subject_must_be_longer_than": 4, 30 | "subject_must_be_shorter_than": 101, 31 | "subject_must_be_in_tense": "imperative", 32 | "subject_must_start_with_case": "upper", 33 | "subject_must_not_end_with_dot": true, 34 | 35 | "body_cannot_be_empty": true, 36 | "body_must_include_reproduction_steps": ["bug"], 37 | 38 | "label_must_be_set": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /keymaps/project-viewer.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-darwin, .platform-win32, .platform-linux": { 3 | "shift-ctrl-alt-c": "project-viewer:autohidePanel", 4 | "shift-ctrl-alt-v": "project-viewer:togglePanel", 5 | "shift-ctrl-alt-n": "project-viewer:openEditor", 6 | "shift-ctrl-alt-m": "project-viewer:focusPanel", 7 | "shift-ctrl-alt-l": "project-viewer:toggleSelectList" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /menus/project-viewer.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": {}, 3 | "menu": [ 4 | { 5 | "label": "Packages", 6 | "submenu": [ 7 | { 8 | "label": "Project Viewer", 9 | "submenu": [ 10 | { 11 | "label": "Toggle Panel", 12 | "command": "project-viewer:togglePanel" 13 | }, 14 | { 15 | "label": "Open editor...", 16 | "command": "project-viewer:openEditor" 17 | }, 18 | { 19 | "label": "Toggle Select View...", 20 | "command": "project-viewer:toggleSelectList" 21 | }, 22 | {"type": "separator"}, 23 | { 24 | "label": "Import / Export...", 25 | "submenu": [ 26 | { 27 | "label": "Import from a private gist", 28 | "command": "project-viewer:gistImport" 29 | }, 30 | { 31 | "label": "Export to a private gist", 32 | "command": "project-viewer:gistExport" 33 | } 34 | ] 35 | }, 36 | { 37 | "label": "Utilities...", 38 | "submenu": [ 39 | { 40 | "label": "Open database file", 41 | "command": "project-viewer:openDatabase" 42 | }, 43 | { 44 | "label": "Convert from 0.3.x local database", 45 | "command": "project-viewer:migrate03x" 46 | }, 47 | { 48 | "label": "Clear all projects stored states", 49 | "command": "project-viewer:clearStates" 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-viewer", 3 | "main": "./src/main", 4 | "version": "1.4.0", 5 | "description": "A project manager that lets you add, edit and remove groups and projects as well as switching between them.", 6 | "keywords": [ 7 | "project", 8 | "productivity", 9 | "management", 10 | "settings", 11 | "workflow" 12 | ], 13 | "repository": "https://github.com/jccguimaraes/atom-project-viewer", 14 | "license": "MIT", 15 | "engines": { 16 | "atom": ">=1.39.1", 17 | "node": ">=10.2.0" 18 | }, 19 | "dependencies": { 20 | "atom-select-list": "latest", 21 | "devicons": "1.8.0" 22 | }, 23 | "devDependencies": { 24 | "all-contributors-cli": "latest" 25 | }, 26 | "consumedServices": { 27 | "status-bar": { 28 | "versions": { 29 | "^1.0.0": "provideStatusBar" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/api-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const api = require('./../src/api'); 4 | 5 | xdescribe ('api', function () { 6 | 7 | it ('should create a group model', function () { 8 | const candidate = {name: 'group #1'}; 9 | const groupModel = api.group.createModel(candidate); 10 | expect(groupModel.name).toBe('group #1'); 11 | }); 12 | 13 | it ('should create a project model', function () { 14 | const candidate = {name: 'project #1'}; 15 | const groupModel = api.project.createModel(candidate); 16 | expect(groupModel.name).toBe('project #1'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /spec/colours-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colours = require('../src/colours'); 4 | 5 | describe ('module: colours', function () { 6 | 7 | describe ('on initialization', function () { 8 | 9 | it ('should add only one stylesheet', function () { 10 | colours.initialize(); 11 | expect(colours.mapper.element).not.toBeUndefined(); 12 | const styles = document.querySelector('head atom-styles').children.length; 13 | colours.initialize(); 14 | const stylesEqual = document.querySelector('head atom-styles').children.length; 15 | 16 | expect(styles).toEqual(stylesEqual); 17 | }); 18 | }); 19 | 20 | describe ('on destruction', function () { 21 | 22 | it ('should remove the stylesheet', function () { 23 | colours.initialize(); 24 | const styles = document.querySelector('head atom-styles').children.length; 25 | colours.destroy(); 26 | const stylesEqual = document.querySelector('head atom-styles').children.length; 27 | 28 | expect(colours.mapper.element).toBeUndefined(); 29 | expect(styles).not.toEqual(stylesEqual); 30 | }); 31 | }); 32 | 33 | describe ('setting a color', function () { 34 | 35 | it ('should add the rule', function () { 36 | colours.initialize(); 37 | const id = 'pv_1'; 38 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(0); 39 | colours.addRule(id, 'group', '#ccc'); 40 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(1); 41 | expect(colours.mapper.element.sheet.cssRules[0].selectorText) 42 | .toEqual(colours.mapper.selectorTexts[id]); 43 | colours.destroy(); 44 | }); 45 | 46 | it ('should remove the rule', function () { 47 | colours.initialize(); 48 | const id = 'pv_1'; 49 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(0); 50 | colours.addRule(id, 'group', '#ccc'); 51 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(1); 52 | colours.removeRule(id); 53 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(0); 54 | colours.destroy(); 55 | }); 56 | 57 | it ('should not duplicate rules for same itemId', function () { 58 | colours.initialize(); 59 | const id = 'pv_1'; 60 | colours.addRule(id, 'group', '#ccc'); 61 | colours.addRule(id, 'group', '#ddd'); 62 | expect(colours.mapper.element.sheet.cssRules).toHaveLength(1); 63 | colours.destroy(); 64 | }); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /spec/common-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cleanConfig = require('../src/common').cleanConfig; 4 | const getModel = require('../src/common').getModel; 5 | const getView = require('../src/common').getView; 6 | const sortList = require('../src/common').sortList; 7 | const model = require('../src/model'); 8 | const view = require('../src/project-view'); 9 | 10 | describe ('common', function () { 11 | 12 | it ('should clean a config entry not defined', function () { 13 | atom.config.set('project-viewer.dummy-config', 'dummy-config'); 14 | expect(atom.config.get('project-viewer.dummy-config')).toBe('dummy-config'); 15 | cleanConfig(); 16 | expect(atom.config.get('project-viewer.dummy-config')).toBeUndefined(); 17 | }); 18 | 19 | it ('should not get any view if not a valid view', function () { 20 | const itemView = document.createElement('div'); 21 | expect(getView(itemView)).toBeNull(); 22 | }); 23 | 24 | it ('should get the view associated with a valid child view', function () { 25 | const itemModel = model.createProject(); 26 | const itemView = view.createView(itemModel); 27 | itemView.initialize(); 28 | itemView.render(); 29 | const insideView = itemView.querySelector('span'); 30 | expect(getView(insideView)).toBe(itemView); 31 | }); 32 | 33 | it ('should get the view passed', function () { 34 | const itemModel = model.createProject(); 35 | const itemView = view.createView(itemModel); 36 | itemView.initialize(); 37 | itemView.render(); 38 | expect(getView(itemView)).toBe(itemView); 39 | }); 40 | 41 | it ('should not get any model if not a valid view', function () { 42 | const itemView = document.createElement('div'); 43 | expect(getModel(itemView)).toBeUndefined(); 44 | }); 45 | 46 | it ('should get the model associated with a valid view', function () { 47 | const itemModel = model.createProject(); 48 | const itemView = view.createView(itemModel); 49 | expect(getModel(itemView)).toBe(itemModel); 50 | }); 51 | 52 | xdescribe ('#sortList', function () { 53 | const listPosition = [ 54 | { name: 'bbbb'}, 55 | { name: 'zzzz'}, 56 | { name: 'aaaa'}, 57 | { name: 'tttt'} 58 | ]; 59 | const listReversePosition = [ 60 | { name: 'tttt'}, 61 | { name: 'aaaa'}, 62 | { name: 'zzzz'}, 63 | { name: 'bbbb'} 64 | ]; 65 | const listAlphabetically = [ 66 | { name: 'aaaa'}, 67 | { name: 'bbbb'}, 68 | { name: 'tttt'}, 69 | { name: 'zzzz'} 70 | ]; 71 | const listReverseAlphabetically = [ 72 | { name: 'zzzz'}, 73 | { name: 'tttt'}, 74 | { name: 'bbbb'}, 75 | { name: 'aaaa'} 76 | ]; 77 | 78 | let list; 79 | 80 | beforeEach(function () { 81 | list = Array.from(listPosition); 82 | }); 83 | 84 | it ('should keep the list\'s position as it is', function () { 85 | sortList(list, 'position'); 86 | expect(list).toEqual(listPosition); 87 | }); 88 | 89 | it ('should reverse the list\'s position', function () { 90 | sortList(list, 'reverse-position'); 91 | expect(list).toEqual(listReversePosition); 92 | }); 93 | 94 | it ('should sort the list in alphabetically order', function () { 95 | sortList(list, 'alphabetically'); 96 | expect(list).toEqual(listAlphabetically); 97 | }); 98 | 99 | it ('should sort the list in reverse alphabetically order', function () { 100 | sortList(list, 'reverse-alphabetically'); 101 | expect(list).toEqual(listReverseAlphabetically); 102 | }); 103 | }); 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /spec/database-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const database = require('../src/database'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | xdescribe ('database', function () { 8 | // 9 | // let fsReadFileError; 10 | // let fsReadFileData; 11 | // 12 | // beforeEach (function () { 13 | // spyOn(fs, 'readFile').andCallFake(function (file, options, cb) { 14 | // cb(fsReadFileError, fsReadFileData); 15 | // }); 16 | // spyOn(fs, 'writeFile').andCallFake(function (file, content) { 17 | // // console.log(file, content); 18 | // }); 19 | // 20 | // spyOn(atom.notifications, 'addError'); 21 | // spyOn(atom.notifications, 'addWarning'); 22 | // spyOn(atom.notifications, 'addSuccess'); 23 | // 24 | // spyOn(atom, 'open'); 25 | // }); 26 | 27 | it ('should open local file', function () { 28 | spyOn(atom, 'open'); 29 | database.openDatabase(); 30 | expect(atom.open).toHaveBeenCalledWith({ 31 | pathsToOpen: path.join(atom.getConfigDirPath(), 'project-viewer.json'), 32 | newWindow: false 33 | }); 34 | }); 35 | 36 | describe ('subscription and unsubscription workflow', function () { 37 | 38 | it ('should subscribe and unsubscribe a callback', function () { 39 | const cb = jasmine.createSpy('subscribeCallBack'); 40 | const unsubscription = database.subscribe(cb); 41 | database.runSubscribers(); 42 | expect(cb).toHaveBeenCalled(); 43 | cb.reset(); 44 | 45 | const result = unsubscription(); 46 | database.runSubscribers(); 47 | 48 | expect(result).toBe(true); 49 | expect(cb).not.toHaveBeenCalled(); 50 | }); 51 | }); 52 | 53 | describe ('directory watching', function () { 54 | const spy = spyOn(fs, 'watch'); 55 | const pathArg = atom.getConfigDirPath(); 56 | database.activate(); 57 | expect(spy).toHaveBeenCalled(); 58 | expect(spy.calls.length).toEqual(1); 59 | expect(spy.calls[0].args[0]).toBe(pathArg); 60 | expect(typeof spy.calls[0].args[1]).toBe('function'); 61 | }); 62 | 63 | 64 | 65 | 66 | 67 | 68 | // it ('should return an empty array if no refresh', function () { 69 | // expect(database.fetch()).toEqual([]); 70 | // }); 71 | // 72 | // it ('should not exist', function () { 73 | // fsReadFileError = {}; 74 | // database.refresh(); 75 | // expect(database.fetch()).toEqual([]); 76 | // expect(atom.notifications.addWarning).toHaveBeenCalled(); 77 | // }); 78 | // 79 | // it ('should exist but empty', function () { 80 | // fsReadFileError = null; 81 | // fsReadFileData = ''; 82 | // database.refresh(); 83 | // expect(database.fetch()).toEqual([]); 84 | // // expect(atom.notifications.addWarning).toHaveBeenCalled(); 85 | // }); 86 | // 87 | // it ('should exist but with wrong schema', function () { 88 | // returnValue = JSON.stringify({}); 89 | // expect(database.refresh(returnValue)).toEqual([]); 90 | // }); 91 | // 92 | // it ('should exist and with a good schema', function () { 93 | // // mimic the atom.getStorageFolder().load 94 | // const mockedRawDB = fs.readFileSync( 95 | // `${__dirname}/mocks/project-viewer.json`, 'utf8' 96 | // ); 97 | // returnValue = JSON.parse(mockedRawDB); 98 | // const store = database.refresh(returnValue); 99 | // expect(store.length).toBe(5); 100 | // }); 101 | // 102 | // describe ('the hierachy', function () { 103 | // 104 | // let mockedRawDB; 105 | // 106 | // beforeEach (function () { 107 | // // mimic the atom.getStorageFolder().load 108 | // mockedRawDB = fs.readFileSync( 109 | // `${__dirname}/mocks/project-viewer.json`, 'utf8' 110 | // ); 111 | // returnValue = JSON.parse(mockedRawDB); 112 | // }); 113 | // 114 | // it ('should have all prototypes defined', function () { 115 | // const store = database.refresh(returnValue); 116 | // expect(Object.getPrototypeOf(store[0])).toBe(Object.prototype); 117 | // expect(Object.getPrototypeOf(store[1])).toBe(store[0]); 118 | // expect(Object.getPrototypeOf(store[2])).toBe(store[1]); 119 | // expect(Object.getPrototypeOf(store[3])).toBe(store[0]); 120 | // expect(Object.getPrototypeOf(store[4])).toBe(Object.prototype); 121 | // }); 122 | // 123 | // it ('should move a model from one parent to another', function () { 124 | // const store = database.refresh(returnValue); 125 | // expect(Object.getPrototypeOf(store[2])).toBe(store[1]); 126 | // const result = database.moveTo(store[2], store[0]); 127 | // expect(result).toBe(true); 128 | // expect(Object.getPrototypeOf(store[2])).toBe(store[0]); 129 | // }); 130 | // 131 | // it ('should save and load the same content if no changes', function () { 132 | // const store = database.refresh(returnValue); 133 | // const oldStore = store.slice(0); 134 | // // database.save(); 135 | // expect(oldStore).toEqual(store); 136 | // }); 137 | // 138 | // it ('should update the local file', function () { 139 | // const store = database.refresh(returnValue); 140 | // const oldStore = store.slice(0); 141 | // database.moveTo(store[2], store[0]); 142 | // expect(oldStore).not.toBe(store); 143 | // }); 144 | // 145 | // it ('sould remove and/or add a model from a parent', function () { 146 | // const store = database.refresh(returnValue); 147 | // const oldStore = store.slice(0); 148 | // const projectsDeleted = database.remove(store[2]); 149 | // expect(oldStore).not.toEqual(store); 150 | // expect(oldStore).toContain(projectsDeleted); 151 | // expect(store).not.toContain(projectsDeleted); 152 | // 153 | // database.addTo(projectsDeleted); 154 | // expect(store).toContain(projectsDeleted); 155 | // 156 | // const groupsDeleted = database.remove(store[0]); 157 | // expect(oldStore).toContain(groupsDeleted); 158 | // expect(store).not.toContain(groupsDeleted); 159 | // database.addTo(groupsDeleted, projectsDeleted); 160 | // expect(store).not.toContain(groupsDeleted); 161 | // database.addTo(groupsDeleted); 162 | // expect(store).toContain(groupsDeleted); 163 | // }); 164 | 165 | // }); 166 | }); 167 | -------------------------------------------------------------------------------- /spec/db-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const database = require('../src/db'); 5 | 6 | describe ('database', function () { 7 | 8 | const toBeMethod = function _toBeMethod() { 9 | return typeof this.actual === 'function'; 10 | } 11 | 12 | beforeEach(function () { 13 | this.addMatchers({ 14 | toBeMethod 15 | }); 16 | }); 17 | 18 | it ('should have the following methods', function () { 19 | expect(database.initialize).toBeMethod(); 20 | expect(database.destroy).toBeMethod(); 21 | expect(database.addToStore).toBeMethod(); 22 | expect(database.removeFromStore).toBeMethod(); 23 | expect(database.moveInStore).toBeMethod(); 24 | }); 25 | 26 | describe ('store workflow', function () { 27 | 28 | beforeEach(function () {}); 29 | 30 | it ('should add an unique item', function () { 31 | // database.addToStore(); 32 | }); 33 | 34 | it ('should remove an item', function () { 35 | // database.removeFromStore(); 36 | }); 37 | 38 | it ('should move an item inside', function () { 39 | // database.moveInStore(); 40 | }); 41 | 42 | describe('moving items in the store', function () { 43 | // note: this is one of the most important 44 | // test case suit in this module, don't let your OCD get to you! 45 | let item_1, item_2, item_3, item_4, item_5; 46 | let item_6, item_7, item_8, item_9, item_10; 47 | 48 | beforeEach(function () { 49 | item_1 = {type: 'group', name: 'group #1'}; 50 | item_2 = {type: 'group', name: 'group #1.1'}; 51 | item_3 = {type: 'project', name: 'project #1.1'}; 52 | item_4 = {type: 'group', name: 'group #1.1.1'}; 53 | item_5 = {type: 'project', name: 'project #1.1.1'}; 54 | item_6 = {type: 'group', name: 'group #2'}; 55 | item_7 = {type: 'group', name: 'group #2.1'}; 56 | item_8 = {type: 'project', name: 'project #2.1'}; 57 | item_9 = {type: 'group', name: 'group #2.1.1'}; 58 | item_10 = {type: 'project', name: 'project #2.1.1'}; 59 | 60 | Object.setPrototypeOf(item_1, Object.prototype); 61 | Object.setPrototypeOf(item_2, item_1); 62 | Object.setPrototypeOf(item_3, item_2); 63 | Object.setPrototypeOf(item_4, item_2); 64 | Object.setPrototypeOf(item_5, item_4); 65 | Object.setPrototypeOf(item_6, Object.prototype); 66 | Object.setPrototypeOf(item_7, item_6); 67 | Object.setPrototypeOf(item_8, item_7); 68 | Object.setPrototypeOf(item_9, item_7); 69 | Object.setPrototypeOf(item_10, item_9); 70 | 71 | database.clearStore(); 72 | 73 | database.addToStore([ 74 | item_1, item_2, item_3, item_4, item_5, 75 | item_6, item_7, item_8, item_9, item_10 76 | ]); 77 | }); 78 | 79 | it ('should not move anything', function () { 80 | const storeAfter = [ 81 | item_1, item_2, item_3, item_4, item_5, 82 | item_6, item_7, item_8, item_9, item_10 83 | ] 84 | database.move(); 85 | expect(database.listStore()).toEqual(storeAfter); 86 | }); 87 | 88 | it ('should not move item_3 when moving to item_8', function () { 89 | const storeAfter = [ 90 | item_1, item_2, item_3, item_4, item_5, 91 | item_6, item_7, item_8, item_9, item_10 92 | ] 93 | database.move(item_3, item_8); 94 | expect(database.listStore()).toEqual(storeAfter); 95 | }); 96 | 97 | it ('should place item_3 in item_4', function () { 98 | const storeAfter = [ 99 | item_1, item_2, item_4, item_5, item_3, 100 | item_6, item_7, item_8, item_9, item_10 101 | ] 102 | database.move(item_3, item_4); 103 | expect(database.listStore()).toEqual(storeAfter); 104 | }); 105 | 106 | it ('should place item_4 in item_7', function () { 107 | const storeAfter = [ 108 | item_1, item_2, item_3, item_6, item_7, 109 | item_8, item_9, item_10, item_4, item_5 110 | ] 111 | database.move(item_4, item_7); 112 | expect(database.listStore()).toEqual(storeAfter); 113 | }); 114 | 115 | it ('should place item_3 after item_8', function () { 116 | const storeAfter = [ 117 | item_1, item_2, item_4, item_5, item_6, 118 | item_7, item_8, item_3, item_9, item_10 119 | ] 120 | database.move(item_3, item_8, false); 121 | expect(database.listStore()).toEqual(storeAfter); 122 | }); 123 | 124 | it ('should place item_3 before item_8', function () { 125 | const storeAfter = [ 126 | item_1, item_2, item_4, item_5, item_6, 127 | item_7, item_3, item_8, item_9, item_10 128 | ] 129 | database.move(item_3, item_8, true); 130 | expect(database.listStore()).toEqual(storeAfter); 131 | }); 132 | 133 | it ('should place item_4 after item_8', function () { 134 | const storeAfter = [ 135 | item_1, item_2, item_3, item_6, item_7, 136 | item_8, item_4, item_5, item_9, item_10 137 | ] 138 | database.move(item_4, item_8, false); 139 | expect(database.listStore()).toEqual(storeAfter); 140 | }); 141 | 142 | it ('should place item_4 before item_8', function () { 143 | const storeAfter = [ 144 | item_1, item_2, item_3, item_6, item_7, 145 | item_4, item_5, item_8, item_9, item_10 146 | ] 147 | database.move(item_4, item_8, true); 148 | expect(database.listStore()).toEqual(storeAfter); 149 | }); 150 | 151 | it ('should place item_2 after item_9', function () { 152 | const storeAfter = [ 153 | item_1, item_6, item_7, item_8, item_9, 154 | item_10, item_2, item_3, item_4, item_5 155 | ] 156 | database.move(item_2, item_9, false); 157 | expect(database.listStore()).toEqual(storeAfter); 158 | }); 159 | 160 | it ('should place item_2 before item_9', function () { 161 | const storeAfter = [ 162 | item_1, item_6, item_7, item_8, item_2, 163 | item_3, item_4, item_5, item_9, item_10 164 | ] 165 | database.move(item_2, item_9, true); 166 | expect(database.listStore()).toEqual(storeAfter); 167 | }); 168 | 169 | it ('should place item_7 after item_3', function () { 170 | const storeAfter = [ 171 | item_1, item_2, item_3, item_7, item_8, 172 | item_9, item_10, item_4, item_5, item_6 173 | ] 174 | database.move(item_7, item_3, false); 175 | expect(database.listStore()).toEqual(storeAfter); 176 | }); 177 | 178 | it ('should place item_7 before item_3', function () { 179 | const storeAfter = [ 180 | item_1, item_2, item_7, item_8, item_9, 181 | item_10, item_3, item_4, item_5, item_6 182 | ] 183 | database.move(item_7, item_3, true); 184 | expect(database.listStore()).toEqual(storeAfter); 185 | }); 186 | }); 187 | }); 188 | 189 | describe ('directory watcher', function () { 190 | 191 | const fsWatchClose = jasmine.createSpy('fsWatchClose'); 192 | 193 | beforeEach(function () { 194 | spyOn(fs, 'watch').andReturn({ 195 | close: fsWatchClose 196 | }); 197 | 198 | spyOn(atom.notifications, 'addError'); 199 | spyOn(atom.notifications, 'addSuccess'); 200 | }); 201 | 202 | it ('should start watching the directory', function () { 203 | expect(database._watcher).toBeUndefined(); 204 | database._startWatcher(); 205 | expect(fs.watch.callCount).toBe(1); 206 | expect(fs.watch).toHaveBeenCalledWith( 207 | atom.getConfigDirPath(), 208 | database._watcherAware 209 | ); 210 | expect(database._watcher).not.toBeUndefined(); 211 | }); 212 | 213 | it ('should stop watching the directory', function () { 214 | database._startWatcher(); 215 | database._closeWatcher(); 216 | expect(fsWatchClose).toHaveBeenCalled(); 217 | expect(database._watcher).toBeUndefined(); 218 | }); 219 | 220 | it ('should _watcherAware', function () { 221 | let eventType; 222 | let filename; 223 | let result; 224 | 225 | result = database._watcherAware(eventType, filename); 226 | expect(result).toBeUndefined(); 227 | 228 | filename = 'dummy-file'; 229 | result = database._watcherAware(eventType, filename); 230 | expect(result).toBeUndefined(); 231 | 232 | filename = 'project-viewer.js'; 233 | result = database._watcherAware(eventType, filename); 234 | expect(result).toBeUndefined(); 235 | 236 | eventType = 'change'; 237 | filename = 'project-viewer.js'; 238 | result = database._watcherAware(eventType, filename); 239 | expect(result).toBeUndefined(); 240 | 241 | eventType = 'rename'; 242 | filename = 'project-viewer.js'; 243 | result = database._watcherAware(eventType, filename); 244 | expect(result).toBe(true); 245 | }) 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /spec/dom-builder-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const domBuilder = require('../src/dom-builder'); 4 | const map = require('./../src/map'); 5 | 6 | describe ('dom-builder', function () { 7 | 8 | it ('should not build an element', function () { 9 | let view = domBuilder.createView( 10 | 'pv-dummy' 11 | ); 12 | expect(view).toBeUndefined(); 13 | }); 14 | 15 | it ('should build an element', function () { 16 | let view = domBuilder.createView( 17 | { 18 | tagIs: 'pv-dummy' 19 | } 20 | ); 21 | expect(view).not.toBeUndefined(); 22 | expect(view instanceof HTMLElement).toBe(true); 23 | expect(view.nodeName).toBe('PV-DUMMY'); 24 | }); 25 | 26 | it ('should build an element with custom methods', function () { 27 | let methods = { 28 | doStuff: function () { return 'stuff'; } 29 | }; 30 | let view = domBuilder.createView( 31 | { 32 | tagIs: 'pv-dummy-2' 33 | }, 34 | methods 35 | ); 36 | expect(view.doStuff()).toBe('stuff'); 37 | }); 38 | 39 | it ('should build an element that extends DIV tag', function () { 40 | let view = domBuilder.createView( 41 | { 42 | tagIs: 'pv-dummy-4', 43 | tagExtends: 'div' 44 | } 45 | ); 46 | expect(view.nodeName).toBe('DIV'); 47 | }); 48 | 49 | it ('should build an element with an associated model', function () { 50 | let model = { 51 | doStuff: function () { return 'stuff'; } 52 | }; 53 | let view = domBuilder.createView( 54 | { 55 | tagIs: 'pv-dummy-3' 56 | }, 57 | undefined, 58 | model 59 | ); 60 | let sameModel = map.get(view); 61 | expect(sameModel).toBe(model); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/group-model-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const modelRef = require('../src/model'); 4 | 5 | xdescribe ('group-model', function () { 6 | it ('should have a type of group right?', function () { 7 | const model = modelRef.createGroup(); 8 | expect(model.type).toBe('group'); 9 | model.type = 'not a group'; 10 | expect(model.type).toBe('group'); 11 | }); 12 | 13 | it ('should not assign unknown properties', function () { 14 | const model = modelRef.createGroup(); 15 | expect(model.dummy).toBeNull(); 16 | model.dummy = 'dummy value'; 17 | expect(model.dummy).toBeNull(); 18 | }); 19 | 20 | it ('should assign a name', function () { 21 | const model = modelRef.createGroup(); 22 | expect(model.name).toBe('unnamed'); 23 | model.name = 'group #1'; 24 | expect(model.name).toBe('group #1'); 25 | }); 26 | 27 | it ('should not assign a name if empty string', function () { 28 | const model = modelRef.createGroup({ 29 | name: '' 30 | }); 31 | expect(model.name).toBe('unnamed'); 32 | }); 33 | 34 | it ('should keep the last name if setting an invalid', function () { 35 | const model = modelRef.createGroup(); 36 | model.name = 1; 37 | expect(model.name).toBe('unnamed'); 38 | model.name = ''; 39 | expect(model.name).toBe('unnamed'); 40 | model.name = '🍺'; 41 | expect(model.name).toBe('🍺'); 42 | model.name = 'group #1'; 43 | model.name = 1; 44 | expect(model.name).toBe('group #1'); 45 | model.name = '

dummy

'; 46 | expect(model.name).toBe('group #1'); 47 | }); 48 | 49 | it ('should not set the name in the prototype chain', function () { 50 | const model1 = modelRef.createGroup(); 51 | const model2 = modelRef.createGroup(); 52 | model2.name = 'group #2'; 53 | expect(model1.name).toBe('unnamed'); 54 | }); 55 | 56 | it ('should assign a sortBy', function () { 57 | const model = modelRef.createGroup(); 58 | expect(model.sortBy).toBe('position'); 59 | model.sortBy = 'alphabetically'; 60 | expect(model.sortBy).toBe('alphabetically'); 61 | }); 62 | 63 | it ('should keep the last sort if setting an invalid', function () { 64 | const model = modelRef.createGroup(); 65 | model.sortBy = 'dummy'; 66 | expect(model.sortBy).toBe('position'); 67 | model.sortBy = 'alphabetically'; 68 | model.sortBy = 'dummy'; 69 | expect(model.sortBy).toBe('alphabetically'); 70 | }); 71 | 72 | it ('should assign an icon', function () { 73 | const model = modelRef.createGroup(); 74 | expect(model.icon).toBe(''); 75 | model.icon = 'octicon-mark-github'; 76 | expect(model.icon).toBe('octicon-mark-github'); 77 | model.icon = 'devicon-angular'; 78 | expect(model.icon).toBe('devicon-angular'); 79 | }); 80 | 81 | it ('should keep the last icon if setting an invalid', function () { 82 | const model = modelRef.createGroup(); 83 | model.icon = 'dummy'; 84 | expect(model.icon).toBe(''); 85 | model.icon = 'octicon-mark-github'; 86 | model.icon = 'dummy'; 87 | expect(model.icon).toBe('octicon-mark-github'); 88 | }); 89 | 90 | it ('should assign a color', function () { 91 | const model = modelRef.createGroup(); 92 | expect(model.color).toBe(''); 93 | model.color = '#fff000'; 94 | expect(model.color).toBe('#fff000'); 95 | model.color = '#fff'; 96 | expect(model.color).toBe('#fff'); 97 | }); 98 | 99 | it ('should keep the last color if setting an invalid', function () { 100 | const model = modelRef.createGroup(); 101 | model.color = '#fff000'; 102 | model.color = 'dummy'; 103 | expect(model.color).toBe('#fff000'); 104 | model.color = '#ffff'; 105 | expect(model.color).toBe('#fff000'); 106 | }); 107 | 108 | it ('should have false as the default expanded state', function () { 109 | const model = modelRef.createGroup(); 110 | expect(model.expanded).toBe(false); 111 | }); 112 | 113 | it ('should set expanded state as true or false', function () { 114 | const model = modelRef.createGroup(); 115 | model.expanded = true; 116 | expect(model.expanded).toBe(true); 117 | model.expanded = 'dummy'; 118 | expect(model.expanded).toBe(true); 119 | model.expanded = false; 120 | expect(model.expanded).toBe(false); 121 | }); 122 | 123 | it ('should get the group\'s breadcrumb', function () { 124 | const model1 = modelRef.createGroup(); 125 | const model2 = modelRef.createGroup(); 126 | expect(model1.breadcrumb()).toEqual('unnamed'); 127 | Object.setPrototypeOf(model1, model2); 128 | expect(model1.breadcrumb()).toEqual('unnamed / unnamed'); 129 | model1.name = 'group #1'; 130 | model2.name = 'group #2'; 131 | expect(model1.breadcrumb()).toEqual('group #2 / group #1'); 132 | }); 133 | 134 | it ('should set as prototype of another group model only', function () { 135 | const model1 = modelRef.createGroup(); 136 | const model2 = modelRef.createGroup(); 137 | model2.name = 'group #2'; 138 | const setPrototype = function (target, proto) { 139 | return Object.setPrototypeOf(target, proto); 140 | }; 141 | expect(setPrototype.bind(null, model1, model2)).not.toThrow(); 142 | expect(Object.getPrototypeOf(model1)).toEqual(model2); 143 | expect(setPrototype.bind(null, model1, {})).toThrow(); 144 | expect(Object.getPrototypeOf(model1)).toEqual(model2); 145 | }); 146 | 147 | it ('should create a model if a candidate is not valid', function () { 148 | const candidate = { 149 | asdasd: 'group candidate #1' 150 | }; 151 | const model = modelRef.createGroup(candidate); 152 | expect(model.name).toBe('unnamed'); 153 | }); 154 | 155 | it ('should create a model if a candidate is valid', function () { 156 | const candidate = { 157 | name: 'group candidate #1' 158 | }; 159 | const model = modelRef.createGroup(candidate); 160 | expect(model.name).toBe(candidate.name); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /spec/group-view-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const groupView = require('../src/group-view'); 4 | const groupModel = require('../src/model'); 5 | 6 | xdescribe ('group-view', function () { 7 | 8 | it ('if no valid model is passed it will throw errors', function () { 9 | const view = groupView.createView(); 10 | const fnInitialize = function _fnInitialize () { 11 | return view.initialize; 12 | } 13 | const fnRender = function _fnRender () { 14 | return view.render; 15 | } 16 | 17 | expect(view).toBeUndefined(); 18 | expect(fnInitialize).toThrow(); 19 | expect(fnRender).toThrow(); 20 | }); 21 | 22 | it ('should return an HTMLElement', function () { 23 | const model = { 24 | uuid: 'pv_' + Math.ceil(Date.now() * Math.random()), 25 | type: 'group' 26 | }; 27 | const view = groupView.createView(model); 28 | 29 | expect(view).toBeInstanceOf(HTMLElement); 30 | expect(view.nodeName).toBe('LI'); 31 | expect(view.getAttribute('is')).toBe('project-viewer-group'); 32 | }); 33 | 34 | it ('should initialize the view', function () { 35 | const model = { 36 | uuid: 'pv_' + Math.ceil(Date.now() * Math.random()), 37 | type: 'group' 38 | }; 39 | const view = groupView.createView(model); 40 | const state = view.initialize(); 41 | expect(state).toBe(true); 42 | }); 43 | 44 | it ('should not render a span if model has no icon', function () { 45 | const model = groupModel.createGroup(); 46 | const view = groupView.createView(model); 47 | view.initialize(); 48 | view.render(); 49 | const listItem = view.querySelector('.list-item'); 50 | expect(listItem.childNodes).toHaveLength(1); 51 | expect(listItem.childNodes[0].nodeName).toBe('#text'); 52 | expect(listItem.childNodes[0].classList).toBeUndefined(); 53 | expect(listItem.childNodes[0].textContent).toBe(model.name); 54 | }); 55 | 56 | it ('should render a span if model has icon', function () { 57 | const model = groupModel.createGroup({ 58 | icon: 'devicon-atom' 59 | }); 60 | const view = groupView.createView(model); 61 | view.initialize(); 62 | view.render(); 63 | const listItem = view.querySelector('.list-item'); 64 | expect(listItem.childNodes).toHaveLength(1); 65 | expect(listItem.childNodes[0].nodeName).toBe('SPAN'); 66 | expect(listItem.childNodes[0].classList.contains(model.icon)).toBe(true); 67 | expect(listItem.childNodes[0].textContent).toBe(model.name); 68 | }); 69 | 70 | it ('should (un)render a span if icon is added or removed', function () { 71 | const icon = 'devicon-atom'; 72 | const model = groupModel.createGroup({ 73 | icon: icon 74 | }); 75 | const view = groupView.createView(model); 76 | view.initialize(); 77 | view.render(); 78 | let listItem = view.querySelector('.list-item'); 79 | expect(listItem.childNodes).toHaveLength(1); 80 | expect(listItem.childNodes[0].nodeName).toBe('SPAN'); 81 | expect(listItem.childNodes[0].classList.contains(icon)).toBe(true); 82 | expect(listItem.childNodes[0].textContent).toBe(model.name); 83 | 84 | delete model.icon; 85 | view.render(); 86 | 87 | expect(listItem.childNodes).toHaveLength(1); 88 | expect(listItem.childNodes[0].nodeName).toBe('#text'); 89 | expect(listItem.childNodes[0].classList).toBeUndefined(); 90 | expect(listItem.childNodes[0].textContent).toBe(model.name); 91 | }); 92 | 93 | it ('should remove click event listener when detached', function () { 94 | const model = { 95 | uuid: 'pv_' + Math.ceil(Date.now() * Math.random()), 96 | type: 'group' 97 | }; 98 | const father = document.createElement('div'); 99 | const view = groupView.createView(model); 100 | view.initialize(); 101 | view.render(); 102 | father.appendChild(view); 103 | var evt = new MouseEvent("click", { 104 | bubbles: true, 105 | cancelable: true, 106 | view: window 107 | }); 108 | expect(view).not.toHaveClass('collapsed'); 109 | view.querySelector('.list-item').dispatchEvent(evt); 110 | expect(view).toHaveClass('collapsed'); 111 | father.removeChild(view); 112 | view.querySelector('.list-item').dispatchEvent(evt); 113 | expect(view).toHaveClass('collapsed'); 114 | }); 115 | 116 | it ('should toggle collapsed', function () { 117 | const model = { 118 | uuid: 'pv_' + Math.ceil(Date.now() * Math.random()), 119 | type: 'group' 120 | }; 121 | const view = groupView.createView(model); 122 | view.initialize(); 123 | view.render(); 124 | expect(view).not.toHaveClass('collapsed'); 125 | view.toggle(); 126 | expect(view).toHaveClass('collapsed'); 127 | view.toggle(); 128 | expect(view).not.toHaveClass('collapsed'); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /spec/main-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | xdescribe ('project-viewer', function () { 4 | 5 | let mainElement; 6 | 7 | beforeEach (function () { 8 | mainElement = atom.views.getView(atom.workspace); 9 | jasmine.attachToDOM(mainElement); 10 | }); 11 | 12 | describe ('activate', function () { 13 | 14 | it ('should append only one project-viewer', function () { 15 | waitsFor (function () { 16 | return atom.packages.activatePackage('project-viewer'); 17 | }); 18 | 19 | runs(function () { 20 | const projectViewer = mainElement.querySelectorAll('project-viewer'); 21 | expect(projectViewer).toHaveLength(1); 22 | atom.workspace.getActivePane().splitRight({copyActiveItem: true}); 23 | expect(projectViewer).toHaveLength(1); 24 | }); 25 | }); 26 | }); 27 | 28 | describe ('deactivate', function () { 29 | 30 | it ('should remove project-viewer', function () { 31 | waitsFor (function () { 32 | return atom.packages.activatePackage('project-viewer'); 33 | }); 34 | 35 | runs(function () { 36 | atom.packages.deactivatePackage('project-viewer'); 37 | const projectViewer = mainElement.querySelector('project-viewer'); 38 | expect(projectViewer).toBeNull(); 39 | }); 40 | }) 41 | }); 42 | 43 | describe ('project-viewer:togglePanel', function () { 44 | 45 | it ('should be visible if visibilityActive is set', function () { 46 | waitsFor (function () { 47 | return atom.packages.activatePackage('project-viewer'); 48 | }); 49 | 50 | runs(function () { 51 | const projectViewer = mainElement.querySelector('project-viewer'); 52 | const parentNode = projectViewer.parentNode; 53 | expect(parentNode).toBeVisible(); 54 | atom.commands.dispatch(mainElement, 'project-viewer:togglePanel'); 55 | expect(parentNode).toBeHidden(); 56 | }); 57 | }); 58 | 59 | it ('should not be visible if visibilityActive is unset', function () { 60 | waitsFor (function () { 61 | atom.config.set('project-viewer.visibilityActive', false); 62 | return atom.packages.activatePackage('project-viewer'); 63 | }); 64 | 65 | runs(function () { 66 | const projectViewer = mainElement.querySelector('project-viewer'); 67 | const parentNode = projectViewer.parentNode; 68 | expect(parentNode).toBeHidden(); 69 | atom.commands.dispatch(mainElement, 'project-viewer:togglePanel'); 70 | expect(parentNode).toBeVisible(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe ('config changes', function () { 76 | 77 | xdescribe ('changing visibilityOption', function () {}); 78 | 79 | describe ('changing visibilityActive', function () { 80 | 81 | it ('should be visible if visibilityActive is set', function () { 82 | 83 | waitsFor (function () { 84 | return atom.packages.activatePackage('project-viewer'); 85 | }); 86 | 87 | runs(function () { 88 | const projectViewer = mainElement.querySelector('project-viewer'); 89 | const parent = projectViewer.parentNode; 90 | expect(parent).toBeVisible(); 91 | }); 92 | }); 93 | 94 | it ('should not be visible if visibilityActive is unset', function () { 95 | 96 | waitsFor (function () { 97 | atom.config.set('project-viewer.visibilityActive', false); 98 | return atom.packages.activatePackage('project-viewer'); 99 | }); 100 | 101 | runs(function () { 102 | const projectViewer = mainElement.querySelector('project-viewer'); 103 | const parent = projectViewer.parentNode; 104 | expect(parent).toBeHidden(); 105 | }); 106 | }); 107 | }); 108 | 109 | describe ('changing panelPosition', function () { 110 | 111 | it ('should start by default as a right panel', function () { 112 | 113 | waitsFor (function () { 114 | return atom.packages.activatePackage('project-viewer'); 115 | }); 116 | 117 | runs(function () { 118 | const projectViewer = mainElement.querySelector('project-viewer'); 119 | let panels = []; 120 | atom.workspace.getRightPanels().forEach(function (panel) { 121 | if (panel.getItem() !== projectViewer) { return; } 122 | panels.push(panel.getItem()); 123 | }); 124 | expect(panels).toHaveLength(1); 125 | 126 | panels = []; 127 | atom.workspace.getLeftPanels().forEach(function (panel) { 128 | if (panel.getItem() !== projectViewer) { return; } 129 | panels.push(panel.getItem()); 130 | }); 131 | expect(panels).toHaveLength(0); 132 | }); 133 | }); 134 | 135 | it ('should start as a left panel if set to Left', function () { 136 | 137 | waitsFor (function () { 138 | atom.config.set('project-viewer.panelPosition', 'Left'); 139 | return atom.packages.activatePackage('project-viewer'); 140 | }); 141 | 142 | runs(function () { 143 | const projectViewer = mainElement.querySelector('project-viewer'); 144 | let panels = []; 145 | atom.workspace.getRightPanels().forEach(function (panel) { 146 | if (panel.getItem() !== projectViewer) { return; } 147 | panels.push(panel.getItem()); 148 | }); 149 | expect(panels).toHaveLength(0); 150 | 151 | panels = []; 152 | atom.workspace.getLeftPanels().forEach(function (panel) { 153 | if (panel.getItem() !== projectViewer) { return; } 154 | panels.push(panel.getItem()); 155 | }); 156 | expect(panels).toHaveLength(1); 157 | 158 | atom.config.set('project-viewer.panelPosition', 'Right'); 159 | 160 | panels = []; 161 | atom.workspace.getRightPanels().forEach(function (panel) { 162 | if (panel.getItem() !== projectViewer) { return; } 163 | panels.push(panel.getItem()); 164 | }); 165 | expect(panels).toHaveLength(1); 166 | 167 | panels = []; 168 | atom.workspace.getLeftPanels().forEach(function (panel) { 169 | if (panel.getItem() !== projectViewer) { return; } 170 | panels.push(panel.getItem()); 171 | }); 172 | expect(panels).toHaveLength(0); 173 | }); 174 | }); 175 | }); 176 | 177 | describe ('changing autoHide', function () { 178 | 179 | it ('should not be partially hidden if autoHide is unset', function () { 180 | 181 | waitsFor (function () { 182 | return atom.packages.activatePackage('project-viewer'); 183 | }); 184 | 185 | runs(function () { 186 | const projectViewer = mainElement.querySelector('project-viewer'); 187 | expect(projectViewer).not.toHaveClass('autohide'); 188 | atom.config.set('project-viewer.autoHide', true); 189 | expect(projectViewer).toHaveClass('autohide'); 190 | }); 191 | }); 192 | 193 | it ('should be partially hidden if autoHide is set', function () { 194 | 195 | waitsFor (function () { 196 | atom.config.set('project-viewer.autoHide', true); 197 | return atom.packages.activatePackage('project-viewer'); 198 | }); 199 | 200 | runs(function () { 201 | const projectViewer = mainElement.querySelector('project-viewer'); 202 | expect(projectViewer).toHaveClass('autohide'); 203 | atom.config.set('project-viewer.autoHide', false); 204 | expect(projectViewer).not.toHaveClass('autohide'); 205 | }); 206 | }); 207 | }); 208 | 209 | describe ('changing hideHeader', function () { 210 | 211 | it ('should show the title by default', function () { 212 | 213 | waitsFor (function () { 214 | return atom.packages.activatePackage('project-viewer'); 215 | }); 216 | 217 | runs(function () { 218 | const projectViewer = mainElement.querySelector('project-viewer'); 219 | const title = projectViewer.querySelector('.heading'); 220 | expect(title).not.toHaveClass('hidden'); 221 | atom.config.set('project-viewer.hideHeader', true); 222 | expect(title).toHaveClass('hidden'); 223 | }); 224 | }); 225 | 226 | it ('should not show the title if set to false', function () { 227 | 228 | waitsFor (function () { 229 | atom.config.set('project-viewer.hideHeader', true); 230 | return atom.packages.activatePackage('project-viewer'); 231 | }); 232 | 233 | runs(function () { 234 | const projectViewer = mainElement.querySelector('project-viewer'); 235 | const title = projectViewer.querySelector('.heading'); 236 | expect(title).toHaveClass('hidden'); 237 | atom.config.set('project-viewer.hideHeader', false); 238 | expect(title).not.toHaveClass('hidden'); 239 | }); 240 | }); 241 | }); 242 | 243 | xdescribe ('changing keepContext', function () {}); 244 | 245 | xdescribe ('changing openNewWindow', function () {}); 246 | 247 | xdescribe ('changing statusBar', function () {}); 248 | }); 249 | 250 | describe ('project-viewer:autohidePanel', function () { 251 | 252 | it ('should toggle partially hidden state', function () { 253 | 254 | waitsFor (function () { 255 | return atom.packages.activatePackage('project-viewer'); 256 | }); 257 | 258 | runs(function () { 259 | const projectViewer = mainElement.querySelector('project-viewer'); 260 | const state = projectViewer.classList.contains('autohide'); 261 | atom.commands.dispatch(mainElement, 'project-viewer:autohidePanel'); 262 | expect(projectViewer.classList.contains('autohide')).not.toBe(state); 263 | }); 264 | }); 265 | }); 266 | 267 | xdescribe ('project-viewer:openForm', function () {}); 268 | 269 | describe ('project-viewer:focusPanel', function () { 270 | 271 | it ('should focus panel', function () { 272 | 273 | waitsFor (function () { 274 | return atom.packages.activatePackage('project-viewer'); 275 | }); 276 | 277 | runs(function () { 278 | const projectViewer = mainElement.querySelector('project-viewer'); 279 | expect(projectViewer).not.toBe(document.activeElement); 280 | atom.commands.dispatch(mainElement, 'project-viewer:focusPanel'); 281 | expect(projectViewer).toBe(document.activeElement); 282 | atom.commands.dispatch(mainElement, 'project-viewer:focusPanel'); 283 | expect(projectViewer).not.toBe(document.activeElement); 284 | }); 285 | }); 286 | }); 287 | 288 | xdescribe ('project-viewer:clearState', function () {}); 289 | 290 | xdescribe ('project-viewer:clearStates', function () {}); 291 | 292 | }); 293 | -------------------------------------------------------------------------------- /spec/project-model-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const modelRef = require('../src/model'); 4 | 5 | xdescribe ('project-model', function () { 6 | it ('should have a type of project right?', function () { 7 | const model = modelRef.createProject(); 8 | expect(model.type).toBe('project'); 9 | model.type = 'not a project'; 10 | expect(model.type).toBe('project'); 11 | }); 12 | 13 | it ('should not assign unknown properties', function () { 14 | const model = modelRef.createProject(); 15 | expect(model.dummy).toBeNull(); 16 | model.dummy = 'dummy value'; 17 | expect(model.dummy).toBeNull(); 18 | }); 19 | 20 | it ('should assign a name', function () { 21 | const model = modelRef.createProject(); 22 | expect(model.name).toBe('unnamed'); 23 | model.name = 'project #1'; 24 | expect(model.name).toBe('project #1'); 25 | }); 26 | 27 | it ('should keep the last name if setting an invalid', function () { 28 | const model = modelRef.createProject(); 29 | model.name = 1; 30 | expect(model.name).toBe('unnamed'); 31 | model.name = '🍺'; 32 | expect(model.name).toBe('🍺'); 33 | model.name = 'project #1'; 34 | model.name = 1; 35 | expect(model.name).toBe('project #1'); 36 | model.name = '

dummy

'; 37 | expect(model.name).toBe('project #1'); 38 | }); 39 | 40 | it ('should not set the name in the prototype chain', function () { 41 | const model1 = modelRef.createProject(); 42 | const model2 = modelRef.createProject(); 43 | model2.name = 'project #2'; 44 | expect(model1.name).toBe('unnamed'); 45 | }); 46 | 47 | it ('should assign a valid array of paths', function () { 48 | const model = modelRef.createProject(); 49 | expect(model.paths).toEqual([]); 50 | // model.paths = ['path/to/somewhere']; 51 | // expect(model.paths).toEqual([]); 52 | // model.paths.push('path/to/somewhere'); 53 | // expect(model.paths).toEqual([]); 54 | model.addPaths(1); 55 | model.addPaths({}); 56 | expect(model.paths).toEqual([]); 57 | model.addPaths('path/to/somewhere'); 58 | expect(model.paths).toEqual(['path/to/somewhere']); 59 | model.addPaths('another/path/to/somewhere'); 60 | expect(model.paths).toEqual( 61 | ['path/to/somewhere', 'another/path/to/somewhere'] 62 | ); 63 | let removedPaths = model.clearPaths(); 64 | expect(model.paths).toEqual([]); 65 | expect(removedPaths).toEqual( 66 | ['path/to/somewhere', 'another/path/to/somewhere'] 67 | ); 68 | model.addPaths( 69 | ['path/to/somewhere', 'another/path/to/somewhere'] 70 | ); 71 | expect(model.paths).toEqual( 72 | ['path/to/somewhere', 'another/path/to/somewhere'] 73 | ); 74 | model.addPaths( 75 | [1, {}] 76 | ); 77 | expect(model.paths).toEqual( 78 | ['path/to/somewhere', 'another/path/to/somewhere'] 79 | ); 80 | model.removePaths( 81 | ['path/to/somewhere', 'another/path/to/somewhere'] 82 | ); 83 | expect(model.paths).toEqual([]); 84 | }); 85 | 86 | it ('should assign a valid array of paths to each item', function () { 87 | const model1 = modelRef.createProject(); 88 | const model2 = modelRef.createProject(); 89 | model1.addPaths('path/to/somewhere'); 90 | expect(model1.paths).toEqual(['path/to/somewhere']); 91 | expect(model2.paths).toEqual([]); 92 | model2.addPaths('another/path/to/somewhere'); 93 | expect(model1.paths).toEqual(['path/to/somewhere']); 94 | expect(model2.paths).toEqual(['another/path/to/somewhere']); 95 | model1.clearPaths(); 96 | expect(model1.paths).toEqual([]); 97 | model1.addPaths(['path/to/somewhere', 'another/path/to/somewhere']); 98 | model1.removePath('path/to/somewhere'); 99 | expect(model1.paths).toEqual(['another/path/to/somewhere']); 100 | model1.addPaths(['path/to/somewhere', 'another/path/to/somewhere']); 101 | model1.removePaths(['path/to/somewhere', 'another/path/to/somewhere']); 102 | expect(model1.paths).toEqual([]); 103 | }); 104 | 105 | it ('should assign an icon', function () { 106 | const model = modelRef.createProject(); 107 | expect(model.icon).toBe(''); 108 | model.icon = 'octicon-mark-github'; 109 | expect(model.icon).toBe('octicon-mark-github'); 110 | model.icon = 'devicon-angular'; 111 | expect(model.icon).toBe('devicon-angular'); 112 | }); 113 | 114 | it ('should keep the last icon if setting an invalid', function () { 115 | const model = modelRef.createProject(); 116 | model.icon = 'dummy'; 117 | expect(model.icon).toBe(''); 118 | model.icon = 'octicon-mark-github'; 119 | model.icon = 'dummy'; 120 | expect(model.icon).toBe('octicon-mark-github'); 121 | }); 122 | 123 | it ('should assign a color', function () { 124 | const model = modelRef.createProject(); 125 | expect(model.color).toBe(''); 126 | model.color = '#fff000'; 127 | expect(model.color).toBe('#fff000'); 128 | model.color = '#fff'; 129 | expect(model.color).toBe('#fff'); 130 | }); 131 | 132 | it ('should keep the last color if setting an invalid', function () { 133 | const model = modelRef.createProject(); 134 | model.color = '#fff000'; 135 | model.color = 'dummy'; 136 | expect(model.color).toBe('#fff000'); 137 | model.color = '#ffff'; 138 | expect(model.color).toBe('#fff000'); 139 | }); 140 | 141 | it ('should have false as the default devMode state', function () { 142 | const model = modelRef.createProject(); 143 | expect(model.devMode).toBe(false); 144 | }); 145 | 146 | it ('should set devMode state as true or false', function () { 147 | const model = modelRef.createProject(); 148 | model.devMode = true; 149 | expect(model.devMode).toBe(true); 150 | model.devMode = 'dummy'; 151 | expect(model.devMode).toBe(true); 152 | model.devMode = false; 153 | expect(model.devMode).toBe(false); 154 | }); 155 | 156 | it ('should set as prototype of another project model only', function () { 157 | const model1 = modelRef.createProject(); 158 | const model2 = modelRef.createProject(); 159 | model2.name = 'project #2'; 160 | const setPrototype = function (target, proto) { 161 | return Object.setPrototypeOf(target, proto); 162 | }; 163 | expect(setPrototype.bind(null, model1, model2)).toThrow(); 164 | expect(Object.getPrototypeOf(model1)).toEqual(Object.prototype); 165 | expect(setPrototype.bind(null, model1, {})).toThrow(); 166 | expect(Object.getPrototypeOf(model1)).toEqual(Object.prototype); 167 | }); 168 | 169 | it ('should get the project\'s breadcrumb', function () { 170 | const model1 = modelRef.createProject(); 171 | const model2 = modelRef.createGroup(); 172 | expect(model1.breadcrumb()).toEqual('unnamed'); 173 | model1.name = 'project #1'; 174 | expect(model1.breadcrumb()).toEqual('project #1'); 175 | Object.setPrototypeOf(model1, model2); 176 | expect(model1.breadcrumb()).toEqual('unnamed / project #1'); 177 | model2.name = 'group #1'; 178 | expect(model1.breadcrumb()).toEqual('group #1 / project #1'); 179 | }); 180 | 181 | it ('should create a model if a candidate is not valid', function () { 182 | const candidate = { 183 | asdasd: 'project candidate #1' 184 | }; 185 | const model = modelRef.createProject(candidate); 186 | expect(model.name).toBe('unnamed'); 187 | }); 188 | 189 | it ('should create a model if a candidate is valid', function () { 190 | const candidate = { 191 | name: 'project candidate #1' 192 | }; 193 | const model = modelRef.createProject(candidate); 194 | expect(model.name).toBe(candidate.name); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const model = require('./model'); 2 | const groupComponent = require('./group-view'); 3 | const projectComponent = require('./project-view'); 4 | const editorComponent = require('./editor-view'); 5 | 6 | const groupModel = function _groupModel (candidate) { 7 | return model.createGroup(candidate); 8 | }; 9 | 10 | const groupView = function _groupView (model) { 11 | return groupComponent.createView(model); 12 | }; 13 | 14 | const projectModel = function _projectModel (candidate) { 15 | return model.createProject(candidate); 16 | }; 17 | 18 | const projectView = function _projectView (model) { 19 | return projectComponent.createView(model); 20 | }; 21 | 22 | const editorView = function _editorView () { 23 | return editorComponent.createView(); 24 | }; 25 | 26 | const group = { 27 | createModel: groupModel, 28 | createView: groupView 29 | }; 30 | 31 | const project = { 32 | createModel: projectModel, 33 | createView: projectView 34 | }; 35 | 36 | const editor = { 37 | createView: editorView 38 | }; 39 | 40 | const api = { 41 | group, 42 | project, 43 | editor 44 | }; 45 | 46 | module.exports = api; 47 | -------------------------------------------------------------------------------- /src/colours.js: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // requires 3 | // ============================================================================= 4 | 5 | // ============================================================================= 6 | // properties 7 | // ============================================================================= 8 | 9 | const mapper = {}; 10 | 11 | // ============================================================================= 12 | // methods 13 | // ============================================================================= 14 | 15 | /** 16 | * Initializes a stylesheet specific for project-viewer 17 | * @public 18 | * @since 1.0.0 19 | */ 20 | const initialize = function _initialize () { 21 | if (!mapper.element) { 22 | mapper.element = document.createElement('style'); 23 | document.querySelector('head atom-styles') 24 | .appendChild(mapper.element); 25 | atom.styles.addStyleElement(mapper.element); 26 | mapper.element.setAttribute('source-path', 'project-viewer-styles'); 27 | mapper.element.setAttribute('priority', 3); 28 | } 29 | mapper.selectorTexts = {}; 30 | mapper.rules = {}; 31 | }; 32 | 33 | /** 34 | * Destroys the stylesheet created and removes it from the DOM 35 | * @public 36 | * @since 1.0.0 37 | */ 38 | const destroy = function _destroy () { 39 | if (mapper.element) { 40 | mapper.element.remove(); 41 | } 42 | delete mapper.element; 43 | delete mapper.selectorTexts; 44 | delete mapper.rules; 45 | }; 46 | 47 | /** 48 | * 49 | * @public 50 | * @since 1.0.0 51 | */ 52 | const addRule = function _addRule (itemId, type, value) { 53 | if (!itemId) { return; } 54 | if (!mapper.element || !mapper.element.sheet) { return; } 55 | 56 | if (!value) { 57 | delete mapper.rules[itemId]; 58 | return; 59 | } 60 | 61 | let selectorText; 62 | let rule; 63 | 64 | switch (type) { 65 | case 'app': 66 | selectorText = `project-viewer, project-viewer.autohide:hover`; 67 | rule = `${selectorText} { width: ${value}px}`; 68 | break; 69 | case 'hotzone': 70 | selectorText = 'project-viewer.autohide'; 71 | rule = `${selectorText} { width: ${value}px}`; 72 | break; 73 | case 'group': 74 | selectorText = `project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li[data-project-viewer-uuid="${itemId}"] .list-item`; 75 | rule = `${selectorText} { color: ${value}}`; 76 | break; 77 | case 'project': 78 | selectorText = `project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li[data-project-viewer-uuid="${itemId}"].list-item`; 79 | rule = `${selectorText} { color: ${value}}`; 80 | break; 81 | case 'project-hover': 82 | selectorText = `project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li.list-item:hover, project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li .list-item:hover` 83 | rule = `${selectorText} { color: ${value}}`; 84 | break; 85 | case 'project-hover-before': 86 | selectorText = `project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li.list-item:not(.selected)::before` 87 | rule = `${selectorText} { background-color: ${value}}`; 88 | break; 89 | case 'project-selected': 90 | selectorText = `project-viewer .list-tree.has-collapsable-children.pv-has-custom-icons li.list-item.selected` 91 | rule = `${selectorText} { color: ${value}}`; 92 | break; 93 | case 'title': 94 | selectorText = `project-viewer .heading`; 95 | rule = `${selectorText} { color: ${value}}`; 96 | break; 97 | } 98 | 99 | mapper.selectorTexts[itemId] = selectorText; 100 | 101 | if (mapper.rules[itemId]) { 102 | this.removeRule(itemId); 103 | } 104 | mapper.rules[itemId] = rule; 105 | 106 | mapper.element.sheet.insertRule( 107 | rule, 108 | mapper.element.sheet.cssRules.length 109 | ); 110 | }; 111 | 112 | /** 113 | * 114 | * @public 115 | * @since 1.0.0 116 | */ 117 | const removeRule = function _removeRule (itemId) { 118 | 119 | if (!mapper || !mapper.selectorTexts) { return; } 120 | 121 | const selectorText = mapper.selectorTexts[itemId]; 122 | 123 | if (!selectorText) { return; } 124 | 125 | Array.from(mapper.element.sheet.cssRules).forEach( 126 | function (cssRule, idx) { 127 | if (cssRule.selectorText === selectorText) { 128 | if (mapper.element && mapper.element.sheet) { 129 | mapper.element.sheet.deleteRule(idx); 130 | } 131 | mapper.selectorTexts[itemId]; 132 | mapper.rules[itemId]; 133 | } 134 | } 135 | ); 136 | }; 137 | 138 | // ============================================================================= 139 | // instantiation 140 | // ============================================================================= 141 | 142 | const colours = { 143 | // properties 144 | mapper, 145 | // privates 146 | // publics 147 | initialize, 148 | destroy, 149 | addRule, 150 | removeRule 151 | }; 152 | 153 | /** 154 | * colours module 155 | * @module database 156 | */ 157 | module.exports = colours; 158 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | const map = require('./map'); 2 | const config = require('./config'); 3 | 4 | const cleanConfig = function _cleanConfig () { 5 | const values = Object.keys(atom.config.getAll('project-viewer')[0].value); 6 | 7 | values.forEach( 8 | value => { 9 | if (value === 'disclaimer') { 10 | const versions = atom.config.get('project-viewer.disclaimer'); 11 | for (let v in versions) { 12 | if (v !== Object.keys(config.disclaimer.properties)[0]) { 13 | atom.config.unset(`project-viewer.disclaimer.${v}`); 14 | } 15 | } 16 | } 17 | if (config.hasOwnProperty(value)) { 18 | return; 19 | } 20 | atom.config.unset(`project-viewer.${value}`); 21 | } 22 | ); 23 | }; 24 | 25 | const getModel = function _getModel (view) { 26 | if (!view) { return undefined; } 27 | return map.get(getView(view)); 28 | }; 29 | 30 | const getView = function _getView (view) { 31 | if (!view) { return undefined; } 32 | while (view && view.nodeName !== 'LI') { 33 | view = view.parentNode; 34 | } 35 | return view; 36 | }; 37 | 38 | const getViewFromModel = function _getViewFromModel (model) { 39 | return document.querySelector( 40 | `project-viewer li[data-project-viewer-uuid="${model.uuid}"]` 41 | ); 42 | }; 43 | 44 | const getSelectedProject = function _getSelectedProject () { 45 | return document.querySelector( 46 | 'project-viewer li[is="project-viewer-project"].selected' 47 | ); 48 | }; 49 | 50 | const getCurrentOpenedProject = function _getCurrentOpenedProject (model) { 51 | return model && Array.isArray(model.paths) && 52 | atom.project.getPaths().length > 0 && model.paths.length > 0 && 53 | model.paths.length === atom.project.getPaths().length && 54 | atom.project.getPaths().every(path => model.paths.indexOf(path) !== -1); 55 | }; 56 | 57 | const buildBlock = function _buildBlock () { 58 | const view = document.createElement('div'); 59 | view.classList.add('block', 'pv-editor-block'); 60 | return view; 61 | }; 62 | 63 | const buildHeader = function _buildHeader (text) { 64 | const view = document.createElement('h2'); 65 | view.textContent = text; 66 | view.classList.add('pv-editor-header'); 67 | return view; 68 | }; 69 | 70 | const buildInput = function _buildInput (type, block) { 71 | const view = document.createElement('input'); 72 | view.setAttribute('type', type); 73 | view.classList.add(`input-${type}`, `pv-input-${block}`); 74 | return view; 75 | }; 76 | 77 | const buildButton = function _buildButton (text, action) { 78 | const view = document.createElement('button'); 79 | view.textContent = text; 80 | view.classList.add('inline-block', 'btn', `btn-${action}`); 81 | return view; 82 | }; 83 | 84 | const buildLabel = function _buildLabel (text, type, child) { 85 | const view = document.createElement('label'); 86 | view.classList.add('input-label', `pv-input-label-${type}`); 87 | const textNode = document.createTextNode(text); 88 | if (child) { view.appendChild(child); } 89 | view.appendChild(textNode); 90 | return view; 91 | }; 92 | 93 | const sorter = function _sorter (reversed, previousView, currentView) { 94 | return (reversed ? -1 : 1) * new Intl.Collator().compare( 95 | getModel(previousView).name, 96 | getModel(currentView).name 97 | ); 98 | }; 99 | 100 | const sortList = function _sortList (list, sortBy) { 101 | const reversed = sortBy.includes('reverse'); 102 | const byPosition = sortBy.includes('position'); 103 | if (!byPosition) { 104 | list = list.sort(sorter.bind(null, reversed)) 105 | return; 106 | } 107 | if (reversed) { 108 | list.reverse(); 109 | } 110 | }; 111 | 112 | exports.cleanConfig = cleanConfig; 113 | exports.getModel = getModel; 114 | exports.getViewFromModel = getViewFromModel; 115 | exports.getSelectedProject = getSelectedProject; 116 | exports.getCurrentOpenedProject = getCurrentOpenedProject; 117 | exports.getView = getView; 118 | exports.buildBlock = buildBlock; 119 | exports.buildHeader = buildHeader; 120 | exports.buildInput = buildInput; 121 | exports.buildButton = buildButton; 122 | exports.buildLabel = buildLabel; 123 | exports.sortList = sortList; 124 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | 'dockOrPanel': { 3 | title: 'Old Panel integration versus Dock', 4 | description: 'If you want to use the old panel integration, just leave uncheck', 5 | type: 'boolean', 6 | default: false, 7 | order: 0 8 | }, 9 | 'visibilityOption': { 10 | title: 'Panel visibility interaction option', 11 | description: 'Define what would be the default action for **project-viewer** visibility on startup.', 12 | type: 'string', 13 | default: 'Display on startup', 14 | enum: [ 15 | 'Display on startup', 16 | 'Remember state' 17 | ], 18 | order: 1 19 | }, 20 | 'visibilityActive': { 21 | title: 'Panel visibility interaction state', 22 | description: 'Relative to the interaction option selected above.', 23 | type: 'boolean', 24 | default: true, 25 | order: 2 26 | }, 27 | 'panelPosition': { 28 | title: 'Panel Position', 29 | description: 'Position the panel to the left or right of the main pane.', 30 | type: 'string', 31 | default: 'Right (last)', 32 | enum: [ 33 | 'Left (first)', 34 | 'Left (last)', 35 | 'Right (first)', 36 | 'Right (last)' 37 | ], 38 | order: 3 39 | }, 40 | 'autoHide': { 41 | title: 'Sidebar auto hidding', 42 | description: 'Panel has auto hide with hover behavior.', 43 | type: 'boolean', 44 | default: false, 45 | order: 4 46 | }, 47 | 'autoHideAbsolute': { 48 | title: 'Makes the Sidebar auto hidding as an absolute', 49 | description: 'This will not make the workspace change width.', 50 | type: 'boolean', 51 | default: false, 52 | order: 5 53 | }, 54 | 'hideHeader': { 55 | title: 'Hide the header', 56 | description: 'You can have more space for the list by hiding the header.', 57 | type: 'boolean', 58 | default: false, 59 | order: 6 60 | }, 61 | 'keepContext': { 62 | title: 'Keep Context', 63 | description: 'When switching from items, if set to `true`, will keep current context. Also will not save contexts between switching.', 64 | type: 'boolean', 65 | default: false, 66 | order: 7 67 | }, 68 | 'keepWindowSize': { 69 | title: 'Keep Window Size', 70 | description: 'When changing projects, if set to `true`, the window size will not change.', 71 | type: 'boolean', 72 | default: false, 73 | order: 7 74 | }, 75 | 'openNewWindow': { 76 | title: 'Open in a new window', 77 | description: 'Always open items in a new window.', 78 | type: 'boolean', 79 | default: false, 80 | order: 8 81 | }, 82 | 'statusBar': { 83 | title: 'Show current project in the status-bar', 84 | description: 'Will show the breadcrumb to the current opened project in the `status-bar`.', 85 | type: 'boolean', 86 | default: false, 87 | order: 9 88 | }, 89 | 'customWidth': { 90 | title: 'Set a custom panel width', 91 | description: 'Define a custom width for the panel.
*double clicking* on the resizer will reset the width', 92 | type: 'number', 93 | default: 200, 94 | order: 10 95 | }, 96 | 'customHotZone': { 97 | title: 'Set a custom hot zone width', 98 | description: 'Cursor movement within this width will make a hidden panel appear', 99 | type: 'number', 100 | default: 20, 101 | order: 10 102 | }, 103 | 'rootSortBy': { 104 | title: 'Root SortBy', 105 | description: 'Sets the root sort by', 106 | type: 'string', 107 | default: 'position', 108 | enum: [ 109 | 'position', 110 | 'reverse-position', 111 | 'alphabetically', 112 | 'reverse-alphabetically' 113 | ], 114 | order: 11 115 | }, 116 | 'githubAccessToken': { 117 | title: 'GitHub Access Token', 118 | description: 'Your personal and private GitHub access token. This is useful if you want to save/backup your projects to a remote place (as a gist). *note*: keep in mind that this token should have only permissions to `rw` gists as well as that any package can access this token string.', 119 | type: 'string', 120 | default: '', 121 | order: 12 122 | }, 123 | 'gistId': { 124 | title: 'Gist ID', 125 | description: 'ID of the gist used as a backup storage.', 126 | type: 'string', 127 | default: '', 128 | order: 13 129 | }, 130 | 'setName': { 131 | description: 'Name of your working set, for example \'work\' or \'home\'. As each working set is backed up into a separate file in gist, you can have multiple Group/Project sets on different machines and have them all safely backed up on gist.', 132 | type: 'string', 133 | default: 'default', 134 | order: 14 135 | }, 136 | 'onlyIcons': { 137 | title: 'Icons list without description', 138 | description: 'Will show only the icons in the icon\'s list', 139 | type: 'boolean', 140 | default: true, 141 | order: 15 142 | }, 143 | 'customPalette': { 144 | title: 'Custom palette to use on editor', 145 | description: 'This can be filled with custom colors', 146 | type: 'array', 147 | default: ['#F1E4E8', '#F7B05B', '#595959', '#CD5334', '#EDB88B', '#23282E', '#263655', 148 | '#F75468', '#FF808F', '#FFDB80', '#292E1E', '#248232', '#2BA84A', '#D8DAD3', 149 | '#FCFFFC', '#8EA604', '#F5BB00', '#EC9F05', '#FF5722', '#BF3100'], 150 | items: { 151 | type: 'string' 152 | }, 153 | order: 16 154 | }, 155 | 'customSelectedColor': { 156 | description: 'Only allows for hexadecimal colors', 157 | type: 'string', 158 | default: '', 159 | order: 17 160 | }, 161 | 'customHoverColor': { 162 | description: 'Only allows for hexadecimal colors', 163 | type: 'string', 164 | default: '', 165 | order: 18 166 | }, 167 | 'customTitleColor': { 168 | description: 'Only allows for hexadecimal colors', 169 | type: 'string', 170 | default: '', 171 | order: 19 172 | }, 173 | 'packagesReload': { 174 | title: 'List of packages to reload', 175 | description: 'This is an attempt to reload any package that stays in the *limbo* of the context switching\n\nExample: pigments, colorio\n\n **Keep in mind that some packages could not work properly. If this happens, please contact me via a feature issue asking to investigate**', 176 | type: 'array', 177 | default: [], 178 | items: { 179 | type: 'string' 180 | }, 181 | order: 20 182 | }, 183 | 'disclaimer': { 184 | title: 'Show release notes on startup', 185 | type: 'object', 186 | properties: { 187 | 'v140': { 188 | title: "for v1.4.0", 189 | type: 'boolean', 190 | default: true 191 | } 192 | }, 193 | order: 21 194 | } 195 | }; 196 | 197 | module.exports = config; 198 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const constants = Object.create(null); 2 | 3 | constants.packageName = 'project-viewer'; 4 | 5 | // this makes constants immutable 6 | Object.freeze(constants); 7 | 8 | /** 9 | * Package common constants 10 | * @module constants 11 | */ 12 | module.exports = constants; 13 | -------------------------------------------------------------------------------- /src/database.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const model = require('./model'); 4 | 5 | const PACKAGE_NAME = 'Project-Viewer'; 6 | const WORKSPACE_URI = 'atom://atom-project-viewer'; 7 | let KEEP_CONTEXT = false; 8 | 9 | const version = '1.1.0'; 10 | const file = 'project-viewer.json'; 11 | const filepath = path.join(atom.getConfigDirPath(), file); 12 | let store = []; 13 | let listeners = []; 14 | let watcher; 15 | let hasLocalFile = false; 16 | 17 | /** 18 | * Maps each model to it's schema object 19 | * @returns {Undefined} cancel if entry has no type or type is not allowed 20 | * @since 1.0.0 21 | */ 22 | const processStorageEntry = function _processStorageEntry (reference, entry) { 23 | let prototypeOf = Object.getPrototypeOf(entry); 24 | let obj; 25 | 26 | if (!entry.hasOwnProperty('type')) { 27 | return; 28 | } 29 | 30 | if (entry.type === 'group') { 31 | obj = model.createGroupSchema(entry); 32 | obj.list = []; 33 | } 34 | else if (entry.type === 'project') { 35 | obj = model.createProjectSchema(entry); 36 | } 37 | else { 38 | return; 39 | } 40 | 41 | reference[entry.uuid] = obj; 42 | 43 | if (prototypeOf === Object.prototype) { 44 | this.push(obj); 45 | return; 46 | } 47 | let prototypeOfObj = reference[prototypeOf.uuid]; 48 | if (!prototypeOfObj) { return; } 49 | 50 | prototypeOfObj.list.push(obj); 51 | }; 52 | 53 | /** 54 | * Changes the current store to be an array with depth depending on each 55 | * entry model's prototype 56 | * @returns {Array} the storage array to be saved locally 57 | * @since 1.0.0 58 | */ 59 | const processStore = function _processStore () { 60 | let storage = []; 61 | const reference = {}; 62 | store.forEach(processStorageEntry.bind(storage, reference)); 63 | return storage; 64 | }; 65 | 66 | /** 67 | * Processes the listed model as a group or project 68 | * @param {Object} listed - a candidate to a model 69 | * @param {Object} parentModel - the model where the candidate will be placed 70 | * @since 1.0.0 71 | */ 72 | const processList = function _processList (parentModel, listed) { 73 | if (listed.type === 'group') { 74 | processGroup(parentModel, listed); 75 | } 76 | if (listed.type === 'project') { 77 | processProject(parentModel, listed); 78 | } 79 | }; 80 | 81 | /** 82 | * Creates a group model from an object data 83 | * @param {Object} protoModel - if exists, it's a referente to a group model 84 | * @param {Object} data - an object with a candidate to become a group 85 | * @since 1.0.0 86 | */ 87 | const processGroup = function _processGroup (protoModel, data) { 88 | if (!data) { return; } 89 | let groupModel = model.createGroup(data); 90 | 91 | addTo(groupModel, protoModel); 92 | 93 | data.list.forEach(processList.bind(null, groupModel)); 94 | }; 95 | 96 | /** 97 | * Creates a project model from an object data 98 | * @param {Object} protoModel - if exists, it's a referente to a group model 99 | * @param {Object} data - an object with a candidate to become a project 100 | * @since 1.0.0 101 | */ 102 | const processProject = function _processProject (protoModel, data) { 103 | if (!data) { return; } 104 | let projectModel = model.createProject(data); 105 | addTo(projectModel, protoModel); 106 | }; 107 | 108 | /** 109 | * Fetches the current store 110 | * @returns {Array} the current store state 111 | * @public 112 | * @since 1.0.0 113 | */ 114 | const fetch = function _fetch () { 115 | return store; 116 | }; 117 | 118 | /** 119 | * Writes content to the local database file 120 | * @param {Object} content - The store content 121 | * @since 1.0.0 122 | */ 123 | const writeToDB = function _writeToDB (content) { 124 | fs.writeFile( 125 | filepath, 126 | JSON.stringify(content, null, 2), 127 | function writeToDBCallback (err) { 128 | if (err) { 129 | atom.notifications.addError('Local database corrupted', { 130 | detail: '😱! Something when wrong while writing to local file!', 131 | icon: 'database' 132 | }); 133 | } 134 | setTimeout(directoryWatch, 1000); 135 | } 136 | ); 137 | }; 138 | 139 | /** 140 | * Updates the local database file with the current content of the store 141 | * @public 142 | * @since 1.0.0 143 | */ 144 | const save = function _save () { 145 | directoryUnwatch(); 146 | writeToDB(exportDB()); 147 | runSubscribers(); 148 | }; 149 | 150 | /** 151 | * Processes the content retrieved from reading the file 152 | * @param {String} result - the content that was retrieved in the file 153 | * @returns {Array} the store 154 | * @since 1.0.0 155 | */ 156 | const processFileContent = function _processFileContent(result) { 157 | try { 158 | let serialized = JSON.parse(result); 159 | store.length = 0; 160 | serialized.root.forEach(processList.bind(null, undefined)); 161 | runSubscribers(); 162 | } catch (e) { 163 | atom.notifications.addError('Local database corrupted', { 164 | detail: 'Please check the content of the local database', 165 | icon: 'database' 166 | }); 167 | } 168 | return store; 169 | } 170 | 171 | /** 172 | * Loads the local database and processes it 173 | * @public 174 | * @since 1.0.0 175 | */ 176 | const refresh = function _refresh () { 177 | fs.readFile(filepath, 'utf8', function (err, data) { 178 | if (err) { 179 | atom.notifications.addWarning('Local database not found', { 180 | description: 'Please go to Packages -> Project Viewer -> Utilities -> Convert from 0.3.x local database if you come from a version previous to 1.0.0', 181 | icon: 'database' 182 | }); 183 | return; 184 | } 185 | hasLocalFile = true; 186 | processFileContent(data); 187 | }); 188 | }; 189 | 190 | /** 191 | * Moves a model from one prototype to another 192 | * @param {Object} childModel - a model object of a group or a project that will 193 | * have it's prototype changed 194 | * @param {Object} protoModel - a model object of a group to be the new prototype 195 | * @return {Null|Boolean} Null if not moved and Boolean if success 196 | * @public 197 | * @since 1.0.0 198 | */ 199 | const moveTo = function _moveTo (movingItem, targetedItem, insertBefore) { 200 | // this is basic stuff 201 | if (!movingItem || !targetedItem) { return; } 202 | 203 | // does this actually happen? as in a DnD action? :see_no_evil: 204 | if (movingItem === targetedItem) { return; } 205 | 206 | // if `isBefore` is `undefined` this means that we are moving an item 207 | // to the targetItem's list and not near it 208 | // and the targetItem must be a group 209 | if (insertBefore === undefined && targetedItem.type !== 'group') { return; } 210 | 211 | let prototypeOfmovingItem; 212 | 213 | // if `isBefore` is `undefined` this means that we are moving an item 214 | // to the targetItem's list and not near it 215 | if (insertBefore === undefined) { 216 | prototypeOfmovingItem = Object.getPrototypeOf(movingItem); 217 | } 218 | 219 | // does this actually happen? as in a DnD action? :see_no_evil: 220 | if (prototypeOfmovingItem === targetedItem) { 221 | return; 222 | } 223 | 224 | let movingItemIdx = store.indexOf(movingItem); 225 | const movingItems = [movingItem]; 226 | 227 | store.slice(movingItemIdx + 1).some( 228 | storeItem => { 229 | const storeItemPrototype = Object.getPrototypeOf(storeItem); 230 | 231 | // this means that no more children of movingItem 232 | if (movingItems.indexOf(storeItemPrototype) === -1) { 233 | return true; 234 | } 235 | 236 | // add to the moving items array 237 | if (movingItems.indexOf(storeItem) === -1) { 238 | movingItems.push(storeItem); 239 | } 240 | 241 | // and we remove from the store 242 | store.splice(store.indexOf(storeItem), 1); 243 | 244 | // keep searching 245 | return false; 246 | } 247 | ); 248 | 249 | store.splice(movingItemIdx, 1); 250 | 251 | let targetedItemIdx = store.indexOf(targetedItem); 252 | let targetedItems = [targetedItem]; 253 | let lastTargetedChildIdx = targetedItemIdx; 254 | 255 | store.slice(targetedItemIdx + 1).some( 256 | storeItem => { 257 | const storeItemPrototype = Object.getPrototypeOf(storeItem); 258 | 259 | if (targetedItems.indexOf(storeItemPrototype) === -1) { 260 | // this means that no more children of targetedItem 261 | return true; 262 | } 263 | 264 | // get the last child's index in the current store 265 | // this is a substore so internal index reference is not valid 266 | lastTargetedChildIdx++; 267 | 268 | // add to the targeted items array 269 | if (targetedItems.indexOf(storeItem) === -1) { 270 | targetedItems.push(storeItem); 271 | } 272 | 273 | // keep searching 274 | return false; 275 | } 276 | ); 277 | 278 | switch (insertBefore) { 279 | case undefined: 280 | store.splice(lastTargetedChildIdx + 1, 0, ...movingItems); 281 | Object.setPrototypeOf(movingItem, targetedItem); 282 | break; 283 | case true: 284 | store.splice(targetedItemIdx, 0, ...movingItems); 285 | Object.setPrototypeOf(movingItem, Object.getPrototypeOf(targetedItem)); 286 | break; 287 | case false: 288 | store.splice(lastTargetedChildIdx + 1, 0, ...movingItems); 289 | Object.setPrototypeOf(movingItem, Object.getPrototypeOf(targetedItem)); 290 | break; 291 | } 292 | }; 293 | 294 | /** 295 | * Removes a model from the store 296 | * @param {Object} model - the model object to remove from the store 297 | * @returns {Null|Object} Undefined if model is an Array, the model if success 298 | * @public 299 | * @since 1.0.0 300 | */ 301 | const remove = function _remove (model) { 302 | const idx = store.indexOf(model); 303 | if (idx === -1) { 304 | return null; 305 | } 306 | const list = store.splice(idx, 1); 307 | 308 | if (list.length === 0) { 309 | return null; 310 | } 311 | 312 | return list[0]; 313 | }; 314 | 315 | /** 316 | * Add a model in the store 317 | * @param {Object} model - the model object candidate to add to the store 318 | * @param {Object} protoModel - group model object to be the prototype of model 319 | * @returns {Undefined|Boolean} Undefined if model is an Array, true if success 320 | * @public 321 | * @since 1.0.0 322 | */ 323 | const addTo = function _addTo (model, protoModel) { 324 | if (Array.isArray(model)) { 325 | model.forEach( 326 | entry => addTo.bind(entry, protoModel) 327 | ); 328 | return; 329 | } 330 | 331 | if (protoModel && protoModel.type === 'project') { return; } 332 | 333 | if (protoModel) { 334 | Object.setPrototypeOf(model, protoModel); 335 | } 336 | 337 | store.push(model); 338 | 339 | return true; 340 | }; 341 | 342 | const applyMigration03x = async function _applyMigration03x (importedDB) { 343 | 344 | const store03x = importedDB || await atom.stateStore.load(file); 345 | 346 | if (!store03x) { 347 | atom.notifications.addInfo('Old database file not found!', { 348 | icon: 'database', 349 | description: 'Could not find any old database file!' 350 | }); 351 | return; 352 | } 353 | 354 | const convertedStore = []; 355 | 356 | function processOldGroup (parentModel, group) { 357 | const groupModel = model.createGroup(group); 358 | convertedStore.push(groupModel); 359 | Object.setPrototypeOf(groupModel, parentModel); 360 | if (group.hasOwnProperty('groups')) { 361 | group.groups.forEach(processOldGroup.bind(null, groupModel)); 362 | } 363 | if (group.hasOwnProperty('projects')) { 364 | group.projects.forEach(processOldProject.bind(null, groupModel)); 365 | } 366 | } 367 | 368 | function processOldProject (parentModel, project) { 369 | const projectModel = model.createProject(project); 370 | convertedStore.push(projectModel); 371 | Object.setPrototypeOf(projectModel, parentModel); 372 | } 373 | 374 | store03x.clients.forEach(processOldGroup.bind(null, Object.prototype)); 375 | store03x.groups.forEach(processOldGroup.bind(null, Object.prototype)); 376 | store03x.projects.forEach(processOldProject.bind(null, Object.prototype)); 377 | 378 | store = convertedStore; 379 | save(); 380 | runSubscribers(); 381 | } 382 | 383 | /** 384 | * Migrate old 0.3.x local database to 1.0.0 385 | * @since 1.0.0 386 | */ 387 | const migrate03x = function _migrate03x (importedDB) { 388 | if (store) { 389 | const notification = atom.notifications.addWarning('Local database found!', { 390 | icon: 'database', 391 | description: 'There is already an **active** database, are you sure you **want** to loose it?', 392 | dismissable: true, 393 | buttons: [ 394 | { 395 | className: 'btn btn-error', 396 | onDidClick: function () { 397 | notification.dismiss(); 398 | }, 399 | text: 'abort' 400 | }, 401 | { 402 | className: 'btn btn-info', 403 | onDidClick: function () { 404 | notification.dismiss(); 405 | applyMigration03x.call(this, importedDB); 406 | }, 407 | text: 'continue' 408 | } 409 | ] 410 | }); 411 | } 412 | }; 413 | 414 | const importDB = function _importDB (importedDB) { 415 | writeToDB(importedDB); 416 | }; 417 | 418 | const exportDB = function _exportDB () { 419 | const storeProcessed = { 420 | info: { 421 | version, 422 | updated: new Date() 423 | }, 424 | root: processStore(store) 425 | }; 426 | return storeProcessed; 427 | }; 428 | 429 | /** 430 | * Deactivation of the database module 431 | * Clears out the directory watcher 432 | * @public 433 | * @since 1.0.0 434 | */ 435 | const deactivate = function _deactivate () { 436 | directoryUnwatch(); 437 | }; 438 | 439 | /** 440 | * Activation of the database module 441 | * Activates the directory watcher 442 | * @public 443 | * @since 1.0.0 444 | */ 445 | const activate = function _activate () { 446 | directoryWatch(); 447 | }; 448 | 449 | /** 450 | * Each watch notification passes through here where it validates if it was 451 | * a change or a rename/deletion. 452 | * @param {String} event - The event occured in the local database, 453 | * values are change and rename 454 | * @param {String} filename - The listener callback 455 | * @since 1.0.0 456 | */ 457 | const directoryWatcher = function _directoryWatcher (event, filename) { 458 | if (filename !== file) { return; } 459 | if (event === 'change') { 460 | refresh(); 461 | return; 462 | } 463 | 464 | if (hasLocalFile) { 465 | hasLocalFile = false; 466 | atom.notifications.addError('Local database not found', { 467 | detail: 'it is possible that the file has been renamed or deleted', 468 | icon: 'database' 469 | }); 470 | } 471 | else { 472 | atom.notifications.addSuccess('Local database found!', { 473 | icon: 'database' 474 | }); 475 | hasLocalFile = true; 476 | refresh(); 477 | } 478 | }; 479 | 480 | /** 481 | * Unwatches for changes in the atom's config directory 482 | * @since 1.0.0 483 | */ 484 | const directoryUnwatch = function _directoryUnwatch () { 485 | if (!watcher) { return; } 486 | watcher.close(); 487 | watcher = undefined; 488 | }; 489 | 490 | /** 491 | * Watches for changes in the atom's config directory 492 | * @since 1.0.0 493 | */ 494 | const directoryWatch = function _directoryWatch () { 495 | if (watcher) { 496 | watcher.close(); 497 | watcher = undefined; 498 | } 499 | watcher = fs.watch(atom.getConfigDirPath(), directoryWatcher); 500 | }; 501 | 502 | /** 503 | * Runs a subscriber callback when a change in the store is made 504 | * @callback subscriber 505 | * @param {subscriber} listener - The listener callback 506 | * @since 1.0.0 507 | */ 508 | const runSubscriber = function _runSubscriber (listener) { 509 | listener(store); 510 | }; 511 | 512 | /** 513 | * Runs all subscribers callback 514 | * @since 1.0.0 515 | */ 516 | const runSubscribers = function _runSubscribers () { 517 | listeners.forEach(runSubscriber); 518 | }; 519 | 520 | /** 521 | * Unsubscribes the callback 522 | * @callback subscriber 523 | * @param {subscriber} listener - The listener callback 524 | * @public 525 | * @since 1.0.0 526 | */ 527 | const unsubscribe = function _unsubscribe (listener) { 528 | const idx = listeners.indexOf(listener); 529 | if (idx === -1) { return; } 530 | listeners.splice(idx, 1); 531 | return true; 532 | }; 533 | 534 | /** 535 | * Subscribes the callback to be invoked on store changes 536 | * @callback subscriber 537 | * @param {subscriber} listener - The listener callback 538 | * @public 539 | * @since 1.0.0 540 | */ 541 | const subscribe = function _subscribe (listener) { 542 | if (listeners.indexOf(listener) !== -1) { 543 | return; 544 | } 545 | listeners.push(listener); 546 | return unsubscribe.bind(this, listener); 547 | }; 548 | 549 | /** 550 | * Opens the local database file 551 | * @since 1.0.0 552 | */ 553 | const openDatabase = function _openDatabase () { 554 | atom.open({ 555 | pathsToOpen: filepath, 556 | newWindow: false 557 | }) 558 | }; 559 | 560 | const database = Object.create(null); 561 | 562 | database.pathsChangedBypass = false; 563 | database.runSubscribers = runSubscribers; 564 | database.subscribe = subscribe; 565 | database.unsubscribe = unsubscribe; 566 | database.openDatabase = openDatabase; 567 | 568 | database.activate = activate; 569 | database.deactivate = deactivate; 570 | database.fetch = fetch; 571 | database.save = save; 572 | database.refresh = refresh; 573 | database.moveTo = moveTo; 574 | database.remove = remove; 575 | database.addTo = addTo; 576 | database.migrate03x = migrate03x; 577 | database.importDB = importDB; 578 | database.exportDB = exportDB; 579 | 580 | database.PACKAGE_NAME = PACKAGE_NAME; 581 | database.WORKSPACE_URI = WORKSPACE_URI; 582 | database.KEEP_CONTEXT = KEEP_CONTEXT; 583 | 584 | /** 585 | * Database / Store module 586 | * @module database 587 | */ 588 | module.exports = database; 589 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // requires 3 | // ============================================================================= 4 | 5 | const fs = require('fs'); 6 | 7 | // ============================================================================= 8 | // properties 9 | // ============================================================================= 10 | 11 | const dbfile = 'project-viewer.js'; 12 | const store = []; 13 | let _worker; 14 | 15 | // ============================================================================= 16 | // methods 17 | // ============================================================================= 18 | 19 | /** 20 | * 21 | * @private 22 | * @since 1.0.0 23 | */ 24 | const _watcherAware = function _watcherAware (eventType, filename) { 25 | if (!eventType || filename !== dbfile) { return; } 26 | if (eventType === 'change') { return; } 27 | return true; 28 | }; 29 | 30 | /** 31 | * 32 | * @private 33 | * @since 1.0.0 34 | */ 35 | const _startWatcher = function _startWatcher () { 36 | this._closeWatcher(); 37 | this._watcher = fs.watch( 38 | atom.getConfigDirPath(), 39 | this._watcherAware 40 | ); 41 | }; 42 | 43 | /** 44 | * 45 | * @private 46 | * @since 1.0.0 47 | */ 48 | const _closeWatcher = function _closeWatcher () { 49 | if (!this._watcher) { return; } 50 | this._watcher.close(); 51 | this._watcher = undefined; 52 | return true; 53 | }; 54 | 55 | /** 56 | * 57 | * @public 58 | * @since 1.0.0 59 | */ 60 | const initialize = function _initialize () {}; 61 | 62 | /** 63 | * 64 | * @since 1.0.0 65 | */ 66 | const destroy = function _destroy () {}; 67 | 68 | /** 69 | * 70 | * @public 71 | * @since 1.0.0 72 | */ 73 | const listStore = function _listStore () { 74 | return store; 75 | }; 76 | 77 | /** 78 | * 79 | * @public 80 | * @since 1.0.0 81 | */ 82 | const clearStore = function _clearStore () { 83 | store.length = 0; 84 | }; 85 | 86 | /** 87 | * 88 | * @public 89 | * @since 1.0.0 90 | */ 91 | const addToStore = function _addToStore (entry/*, delegator*/) { 92 | if (!entry) return; 93 | if (Array.isArray(entry)) { 94 | entry.forEach(this.addToStore); 95 | return; 96 | } 97 | // if (delegator) 98 | // return entry; 99 | store.push(entry); 100 | }; 101 | 102 | /** 103 | * 104 | * @public 105 | * @since 1.0.0 106 | */ 107 | const removeFromStore = function _removeFromStore (entry) { 108 | return entry; 109 | }; 110 | 111 | /** 112 | * 113 | * @public 114 | * @since 1.0.0 115 | */ 116 | const moveInStore = function _moveInStore (entry, delegator) { 117 | if (delegator) 118 | return entry; 119 | }; 120 | 121 | const move = function _move (movingItem, targetedItem, insertBefore) { 122 | // this is basic stuff 123 | if (!movingItem || !targetedItem) { return; } 124 | 125 | // does this actually happen? as in a DnD action? :see_no_evil: 126 | if (movingItem === targetedItem) { return; } 127 | 128 | // if `isBefore` is `undefined` this means that we are moving an item 129 | // to the targetItem's list and not near it 130 | // and the targetItem must be a group 131 | if (insertBefore === undefined && targetedItem.type !== 'group') { return; } 132 | 133 | let prototypeOfmovingItem; 134 | 135 | // if `isBefore` is `undefined` this means that we are moving an item 136 | // to the targetItem's list and not near it 137 | if (insertBefore === undefined) { 138 | prototypeOfmovingItem = Object.getPrototypeOf(movingItem); 139 | } 140 | 141 | // does this actually happen? as in a DnD action? :see_no_evil: 142 | if (prototypeOfmovingItem === targetedItem) { 143 | return; 144 | } 145 | 146 | let movingItemIdx = store.indexOf(movingItem); 147 | const movingItems = [movingItem]; 148 | 149 | store.slice(movingItemIdx + 1).some( 150 | storeItem => { 151 | const storeItemPrototype = Object.getPrototypeOf(storeItem); 152 | 153 | // this means that no more children of movingItem 154 | if (movingItems.indexOf(storeItemPrototype) === -1) { 155 | return true; 156 | } 157 | 158 | // add to the moving items array 159 | if (movingItems.indexOf(storeItem) === -1) { 160 | movingItems.push(storeItem); 161 | } 162 | 163 | // and we remove from the store 164 | store.splice(store.indexOf(storeItem), 1); 165 | 166 | // keep searching 167 | return false; 168 | } 169 | ); 170 | 171 | store.splice(movingItemIdx, 1); 172 | 173 | let targetedItemIdx = store.indexOf(targetedItem); 174 | let targetedItems = [targetedItem]; 175 | let lastTargetedChildIdx = targetedItemIdx; 176 | 177 | store.slice(targetedItemIdx + 1).some( 178 | storeItem => { 179 | const storeItemPrototype = Object.getPrototypeOf(storeItem); 180 | 181 | if (targetedItems.indexOf(storeItemPrototype) === -1) { 182 | // this means that no more children of targetedItem 183 | return true; 184 | } 185 | 186 | // get the last child's index in the current store 187 | // this is a substore so internal index reference is not valid 188 | lastTargetedChildIdx++; 189 | 190 | // add to the targeted items array 191 | if (targetedItems.indexOf(storeItem) === -1) { 192 | targetedItems.push(storeItem); 193 | } 194 | 195 | // keep searching 196 | return false; 197 | } 198 | ); 199 | 200 | switch (insertBefore) { 201 | case undefined: 202 | store.splice(lastTargetedChildIdx + 1, 0, ...movingItems); 203 | Object.setPrototypeOf(movingItem, targetedItem); 204 | break; 205 | case true: 206 | store.splice(targetedItemIdx, 0, ...movingItems); 207 | Object.setPrototypeOf(movingItem, Object.getPrototypeOf(targetedItem)); 208 | break; 209 | case false: 210 | store.splice(lastTargetedChildIdx + 1, 0, ...movingItems); 211 | Object.setPrototypeOf(movingItem, Object.getPrototypeOf(targetedItem)); 212 | break; 213 | } 214 | }; 215 | 216 | // ============================================================================= 217 | // instantiation 218 | // ============================================================================= 219 | 220 | const database = { 221 | // properties 222 | _worker, 223 | // privates 224 | _watcherAware, 225 | _startWatcher, 226 | _closeWatcher, 227 | // publics 228 | initialize, 229 | destroy, 230 | clearStore, 231 | listStore, 232 | addToStore, 233 | removeFromStore, 234 | moveInStore, 235 | move 236 | }; 237 | 238 | /** 239 | * Database / Store module 240 | * @module database 241 | */ 242 | module.exports = database; 243 | -------------------------------------------------------------------------------- /src/dom-builder.js: -------------------------------------------------------------------------------- 1 | const map = require('./map'); 2 | 3 | const createView = function _createView (element, methods, model) { 4 | const tagExtends = element.tagExtends; 5 | const tagIs = element.tagIs; 6 | let view; 7 | let options = {}; 8 | 9 | if (!tagIs) { 10 | return; 11 | } 12 | 13 | if (methods) { 14 | options.prototype = methods; 15 | } 16 | 17 | if (tagExtends) { 18 | options.extends = tagExtends; 19 | } 20 | 21 | try { 22 | const viewConstructor = document.registerElement( 23 | tagIs, 24 | options 25 | ); 26 | Object.setPrototypeOf(methods, HTMLElement.prototype); 27 | view = new viewConstructor(); 28 | } catch (e) { 29 | if (tagExtends) { 30 | view = document.createElement(tagExtends, tagIs); 31 | } 32 | else { 33 | view = document.createElement(tagIs); 34 | } 35 | } 36 | 37 | if (model) { 38 | map.set(view, model); 39 | } 40 | return view; 41 | }; 42 | 43 | module.exports = { 44 | createView: createView 45 | }; 46 | -------------------------------------------------------------------------------- /src/group-view.js: -------------------------------------------------------------------------------- 1 | const map = require('./map'); 2 | const database = require('./database'); 3 | const domBuilder = require('./dom-builder'); 4 | const colours = require('./colours'); 5 | const {getModel, getView, sortList} = require('./common'); 6 | 7 | const dragstart = function _dragstart (evt) { 8 | evt.dataTransfer.setData( 9 | "text/plain", 10 | getModel(evt.target).uuid 11 | ); 12 | // evt.dataTransfer.dropEffect = "move"; 13 | const view = getView(evt.target); 14 | view.classList.add('dragged'); 15 | evt.stopPropagation(); 16 | }; 17 | 18 | const dragover = function _dragover (evt) { 19 | const view = getView(evt.target); 20 | 21 | const threshold = view.querySelector('.list-item').clientHeight / 3; 22 | 23 | if (view.offsetTop + threshold > evt.layerY) { 24 | view.classList.add('above'); 25 | view.classList.remove('center'); 26 | view.classList.remove('below'); 27 | } 28 | else if (view.offsetTop + (2 * threshold) > evt.layerY) { 29 | view.classList.remove('above'); 30 | view.classList.add('center'); 31 | view.classList.remove('below'); 32 | } 33 | else { 34 | view.classList.remove('above'); 35 | view.classList.remove('center'); 36 | view.classList.add('below'); 37 | } 38 | 39 | evt.preventDefault(); 40 | }; 41 | 42 | const dragleave = function _dragleave (evt) { 43 | const view = getView(evt.target); 44 | view.classList.remove('above', 'center', 'below'); 45 | evt.stopPropagation(); 46 | }; 47 | 48 | const dragenter = function _dragenter (evt) { 49 | evt.stopPropagation(); 50 | }; 51 | 52 | const dragend = function _dragend (evt) { 53 | const view = getView(evt.target); 54 | view.classList.remove('above', 'center', 'below', 'dragged'); 55 | evt.stopPropagation(); 56 | }; 57 | 58 | const drop = function _drop (evt) { 59 | evt.stopPropagation(); 60 | const uuid = evt.dataTransfer.getData("text/plain"); 61 | const draggedView = document.querySelector( 62 | `project-viewer li[data-project-viewer-uuid="${uuid}"]` 63 | ); 64 | 65 | if (!draggedView) { return; } 66 | 67 | const droppedModel = getModel(evt.target); 68 | const draggedModel = getModel(draggedView); 69 | 70 | const droppedView = getView(evt.target); 71 | 72 | if (!droppedView) { return; } 73 | 74 | if (droppedModel.type !== 'group') { return; } 75 | 76 | if (droppedView === draggedView) { return; } 77 | 78 | // a bit hacky 79 | let insertBefore = undefined; 80 | 81 | if (droppedView.classList.contains('above')) { 82 | insertBefore = true; 83 | } 84 | else if (droppedView.classList.contains('below')) { 85 | insertBefore = false; 86 | } 87 | 88 | this.classList.remove('dropping', 'below', 'above', 'center'); 89 | 90 | database.moveTo(draggedModel, droppedModel, insertBefore); 91 | database.save(); 92 | }; 93 | 94 | const viewMethods = { 95 | attachedCallback: function _attachedCallback () { 96 | this.addEventListener('dragstart', dragstart, false); 97 | this.addEventListener('dragover', dragover, false); 98 | this.addEventListener('dragleave', dragleave, false); 99 | this.addEventListener('dragenter', dragenter, false); 100 | this.addEventListener('dragend', dragend, false); 101 | this.addEventListener('drop', drop, false); 102 | }, 103 | detachedCallback: function _detachedCallback () { 104 | let contentNode = this.querySelector('.list-item'); 105 | if (contentNode) { 106 | contentNode.removeEventListener('click', this.expandOrCollapse.bind(this)); 107 | } 108 | this.removeEventListener('dragstart', dragstart, false); 109 | this.removeEventListener('dragover', dragover, false); 110 | this.removeEventListener('dragleave', dragleave, false); 111 | this.removeEventListener('dragenter', dragenter, false); 112 | this.removeEventListener('dragend', dragend, false); 113 | this.removeEventListener('drop', drop, false); 114 | }, 115 | expandOrCollapse: function _expandOrCollapse (evt) { 116 | evt.preventDefault(); 117 | evt.stopPropagation(); 118 | const model = map.get(getView(evt.target)); 119 | if (!model) { return; } 120 | model.expanded = !model.expanded; 121 | this.classList.toggle('collapsed'); 122 | database.save(); 123 | }, 124 | initialize: function _initialize () { 125 | 126 | const model = map.get(this); 127 | if (!model) { return; } 128 | 129 | let listItem = document.createElement('div'); 130 | listItem.classList.add('list-item'); 131 | listItem.addEventListener('click', this.expandOrCollapse.bind(this)); 132 | 133 | this.classList.add('list-nested-item'); 134 | this.classList.toggle('collapsed', !model.expanded); 135 | this.setAttribute('data-project-viewer-uuid', model.uuid); 136 | this.setAttribute('draggable', 'true'); 137 | this.appendChild(listItem); 138 | 139 | return true; 140 | }, 141 | render: function _render () { 142 | const model = map.get(this); 143 | 144 | if (!model) { 145 | return; 146 | } 147 | 148 | let spanNode = this.querySelector('.list-item span'); 149 | let contentNode = this.querySelector('.list-item'); 150 | 151 | if (spanNode && spanNode.parentNode !== this) { 152 | spanNode = undefined; 153 | } 154 | 155 | if (!contentNode) { 156 | return; 157 | } 158 | 159 | if (model.icon && !spanNode) { 160 | contentNode.textContent = ''; 161 | spanNode = document.createElement('span'); 162 | contentNode.appendChild(spanNode); 163 | } 164 | 165 | if (model.icon) { 166 | contentNode = spanNode; 167 | if (model.icon.startsWith('devicons-')) { 168 | contentNode.classList.add('devicons', model.icon); 169 | } 170 | else { 171 | contentNode.classList.add('icon', model.icon); 172 | } 173 | } 174 | else if (spanNode) { 175 | contentNode.removeChild(spanNode); 176 | } 177 | 178 | if (model.name) { 179 | contentNode.textContent = model.name; 180 | } 181 | 182 | if (model.color) { 183 | colours.addRule(model.uuid, model.type, model.color); 184 | } else { 185 | colours.removeRule(model.uuid); 186 | } 187 | 188 | let listTree = this.querySelector('.list-tree'); 189 | 190 | if (!listTree) { 191 | listTree = document.createElement('ul'); 192 | listTree.classList.add('list-tree'); 193 | this.appendChild(listTree); 194 | } 195 | }, 196 | sortChildren: function _sortChildren () { 197 | const listTree = this.querySelector('.list-tree'); 198 | if (!listTree) { return; } 199 | const children = Array.from(listTree.children); 200 | if (!children || children.length === 0) { return; } 201 | 202 | sortList(children, map.get(this).sortBy); 203 | children.forEach(view => listTree.appendChild(view)); 204 | }, 205 | attachChild: function _attachChild (node) { 206 | const listTree = this.querySelector('.list-tree'); 207 | if (!listTree) { return; } 208 | listTree.appendChild(node); 209 | }, 210 | detachChild: function _detachChild (node) { 211 | let listTree = this.querySelector('.list-tree'); 212 | if (!listTree) { 213 | return; 214 | } 215 | listTree.removeChild(node); 216 | }, 217 | sorting: function _sorting () { 218 | const model = map.get(this); 219 | 220 | if (!model) { return; } 221 | 222 | return model.name; 223 | } 224 | }; 225 | 226 | const createView = function _createView (model) { 227 | let options = { 228 | tagExtends: 'li', 229 | tagIs: 'project-viewer-group' 230 | }; 231 | if (!model) { 232 | return; 233 | } 234 | return domBuilder.createView(options, viewMethods, model); 235 | }; 236 | 237 | module.exports = { 238 | createView: createView 239 | }; 240 | -------------------------------------------------------------------------------- /src/json/devicons.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.8.0", 3 | "list": [ 4 | "devicons-git", 5 | "devicons-git_compare", 6 | "devicons-git_branch", 7 | "devicons-git_commit", 8 | "devicons-git_pull_request", 9 | "devicons-git_merge", 10 | "devicons-bitbucket", 11 | "devicons-github_alt", 12 | "devicons-github_badge", 13 | "devicons-github", 14 | "devicons-github_full", 15 | "devicons-java", 16 | "devicons-ruby", 17 | "devicons-scala", 18 | "devicons-python", 19 | "devicons-go", 20 | "devicons-ruby_on_rails", 21 | "devicons-django", 22 | "devicons-markdown", 23 | "devicons-php", 24 | "devicons-mysql", 25 | "devicons-streamline", 26 | "devicons-database", 27 | "devicons-laravel", 28 | "devicons-javascript", 29 | "devicons-angular", 30 | "devicons-backbone", 31 | "devicons-coffeescript", 32 | "devicons-jquery", 33 | "devicons-modernizr", 34 | "devicons-jquery_ui", 35 | "devicons-ember", 36 | "devicons-dojo", 37 | "devicons-nodejs", 38 | "devicons-nodejs_small", 39 | "devicons-javascript_shield", 40 | "devicons-bootstrap", 41 | "devicons-sass", 42 | "devicons-css3_full", 43 | "devicons-css3", 44 | "devicons-html5", 45 | "devicons-html5_multimedia", 46 | "devicons-html5_device_access", 47 | "devicons-html5_3d_effects", 48 | "devicons-html5_connectivity", 49 | "devicons-ghost_small", 50 | "devicons-ghost", 51 | "devicons-magento", 52 | "devicons-joomla", 53 | "devicons-jekyll_small", 54 | "devicons-drupal", 55 | "devicons-wordpress", 56 | "devicons-grunt", 57 | "devicons-bower", 58 | "devicons-npm", 59 | "devicons-yahoo_small", 60 | "devicons-yahoo", 61 | "devicons-bing_small", 62 | "devicons-windows", 63 | "devicons-linux", 64 | "devicons-ubuntu", 65 | "devicons-android", 66 | "devicons-apple", 67 | "devicons-appstore", 68 | "devicons-phonegap", 69 | "devicons-blackberry", 70 | "devicons-stackoverflow", 71 | "devicons-techcrunch", 72 | "devicons-codrops", 73 | "devicons-css_tricks", 74 | "devicons-smashing_magazine", 75 | "devicons-netmagazine", 76 | "devicons-codepen", 77 | "devicons-cssdeck", 78 | "devicons-hackernews", 79 | "devicons-dropbox", 80 | "devicons-google_drive", 81 | "devicons-visualstudio", 82 | "devicons-unity_small", 83 | "devicons-raspberry_pi", 84 | "devicons-chrome", 85 | "devicons-ie", 86 | "devicons-firefox", 87 | "devicons-opera", 88 | "devicons-safari", 89 | "devicons-swift", 90 | "devicons-symfony", 91 | "devicons-symfony_badge", 92 | "devicons-less", 93 | "devicons-stylus", 94 | "devicons-trello", 95 | "devicons-atlassian", 96 | "devicons-jira", 97 | "devicons-envato", 98 | "devicons-snap_svg", 99 | "devicons-raphael", 100 | "devicons-google_analytics", 101 | "devicons-compass", 102 | "devicons-onedrive", 103 | "devicons-gulp", 104 | "devicons-atom", 105 | "devicons-cisco", 106 | "devicons-nancy", 107 | "devicons-clojure", 108 | "devicons-clojure_alt", 109 | "devicons-perl", 110 | "devicons-celluloid", 111 | "devicons-w3c", 112 | "devicons-redis", 113 | "devicons-postgresql", 114 | "devicons-webplatform", 115 | "devicons-jenkins", 116 | "devicons-requirejs", 117 | "devicons-opensource", 118 | "devicons-typo3", 119 | "devicons-uikit", 120 | "devicons-doctrine", 121 | "devicons-groovy", 122 | "devicons-nginx", 123 | "devicons-haskell", 124 | "devicons-zend", 125 | "devicons-gnu", 126 | "devicons-yeoman", 127 | "devicons-heroku", 128 | "devicons-debian", 129 | "devicons-travis", 130 | "devicons-dotnet", 131 | "devicons-codeigniter", 132 | "devicons-javascript_badge", 133 | "devicons-yii", 134 | "devicons-msql_server", 135 | "devicons-composer", 136 | "devicons-krakenjs_badge", 137 | "devicons-krakenjs", 138 | "devicons-mozilla", 139 | "devicons-firebase", 140 | "devicons-sizzlejs", 141 | "devicons-creativecommons", 142 | "devicons-creativecommons_badge", 143 | "devicons-mitlicence", 144 | "devicons-senchatouch", 145 | "devicons-bugsense", 146 | "devicons-extjs", 147 | "devicons-mootools_badge", 148 | "devicons-mootools", 149 | "devicons-ruby_rough", 150 | "devicons-komodo", 151 | "devicons-coda", 152 | "devicons-bintray", 153 | "devicons-terminal", 154 | "devicons-code", 155 | "devicons-responsive", 156 | "devicons-dart", 157 | "devicons-aptana", 158 | "devicons-mailchimp", 159 | "devicons-netbeans", 160 | "devicons-dreamweaver", 161 | "devicons-brackets", 162 | "devicons-eclipse", 163 | "devicons-cloud9", 164 | "devicons-scrum", 165 | "devicons-prolog", 166 | "devicons-terminal_badge", 167 | "devicons-code_badge", 168 | "devicons-mongodb", 169 | "devicons-meteor", 170 | "devicons-meteorfull", 171 | "devicons-fsharp", 172 | "devicons-rust", 173 | "devicons-ionic", 174 | "devicons-sublime" 175 | ] 176 | } 177 | -------------------------------------------------------------------------------- /src/json/octicons.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.4.0", 3 | "list": [ 4 | "icon-alert", 5 | "icon-alignment-align", 6 | "icon-alignment-aligned-to", 7 | "icon-alignment-unalign", 8 | "icon-arrow-down", 9 | "icon-arrow-left", 10 | "icon-arrow-right", 11 | "icon-arrow-small-down", 12 | "icon-arrow-small-left", 13 | "icon-arrow-small-right", 14 | "icon-arrow-small-up", 15 | "icon-arrow-up", 16 | "icon-beaker", 17 | "icon-beer", 18 | "icon-bell", 19 | "icon-bold", 20 | "icon-book", 21 | "icon-bookmark", 22 | "icon-briefcase", 23 | "icon-broadcast", 24 | "icon-browser", 25 | "icon-bug", 26 | "icon-calendar", 27 | "icon-check", 28 | "icon-checklist", 29 | "icon-chevron-down", 30 | "icon-chevron-left", 31 | "icon-chevron-right", 32 | "icon-chevron-up", 33 | "icon-circle-slash", 34 | "icon-circuit-board", 35 | "icon-clippy", 36 | "icon-clock", 37 | "icon-cloud-download", 38 | "icon-cloud-upload", 39 | "icon-code", 40 | "icon-color-mode", 41 | "icon-comment-add", 42 | "icon-comment-discussion", 43 | "icon-comment", 44 | "icon-credit-card", 45 | "icon-dash", 46 | "icon-dashboard", 47 | "icon-database", 48 | "icon-desktop-download", 49 | "icon-device-camera-video", 50 | "icon-device-camera", 51 | "icon-device-desktop", 52 | "icon-device-mobile", 53 | "icon-diff-added", 54 | "icon-diff-ignored", 55 | "icon-diff-modified", 56 | "icon-diff-removed", 57 | "icon-diff-renamed", 58 | "icon-diff", 59 | "icon-ellipses", 60 | "icon-ellipsis", 61 | "icon-eye-unwatch", 62 | "icon-eye-watch", 63 | "icon-eye", 64 | "icon-file-add", 65 | "icon-file-binary", 66 | "icon-file-code", 67 | "icon-file-directory-create", 68 | "icon-file-directory", 69 | "icon-file-media", 70 | "icon-file-pdf", 71 | "icon-file-submodule", 72 | "icon-file-symlink-directory", 73 | "icon-file-symlink-file", 74 | "icon-file-text", 75 | "icon-file-zip", 76 | "icon-file", 77 | "icon-flame", 78 | "icon-fold", 79 | "icon-gear", 80 | "icon-gift", 81 | "icon-gist-fork", 82 | "icon-gist-new", 83 | "icon-gist-private", 84 | "icon-gist-secret", 85 | "icon-gist", 86 | "icon-git-branch-create", 87 | "icon-git-branch-delete", 88 | "icon-git-branch", 89 | "icon-git-commit", 90 | "icon-git-compare", 91 | "icon-git-fork-private", 92 | "icon-git-merge", 93 | "icon-git-pull-request-abandoned", 94 | "icon-git-pull-request", 95 | "icon-globe", 96 | "icon-grabber", 97 | "icon-graph", 98 | "icon-heart", 99 | "icon-history", 100 | "icon-home", 101 | "icon-horizontal-rule", 102 | "icon-hourglass", 103 | "icon-hubot", 104 | "icon-inbox", 105 | "icon-info", 106 | "icon-issue-closed", 107 | "icon-issue-opened", 108 | "icon-issue-reopened", 109 | "icon-italic", 110 | "icon-jersey", 111 | "icon-jump-down", 112 | "icon-jump-left", 113 | "icon-jump-right", 114 | "icon-jump-up", 115 | "icon-key", 116 | "icon-keyboard", 117 | "icon-law", 118 | "icon-light-bulb", 119 | "icon-link-external", 120 | "icon-link", 121 | "icon-list-ordered", 122 | "icon-list-unordered", 123 | "icon-location", 124 | "icon-lock", 125 | "icon-log-in", 126 | "icon-log-out", 127 | "icon-logo-gist", 128 | "icon-mail-read", 129 | "icon-mail-reply", 130 | "icon-mail", 131 | "icon-mark-github", 132 | "icon-markdown", 133 | "icon-megaphone", 134 | "icon-mention", 135 | "icon-microscope", 136 | "icon-milestone", 137 | "icon-mirror-private", 138 | "icon-mirror-public", 139 | "icon-mirror", 140 | "icon-mortar-board", 141 | "icon-move-down", 142 | "icon-move-left", 143 | "icon-move-right", 144 | "icon-move-up", 145 | "icon-mute", 146 | "icon-no-newline", 147 | "icon-octoface", 148 | "icon-organization", 149 | "icon-package", 150 | "icon-paintcan", 151 | "icon-pencil", 152 | "icon-person-add", 153 | "icon-person-follow", 154 | "icon-person", 155 | "icon-pin", 156 | "icon-playback-fast-forward", 157 | "icon-playback-pause", 158 | "icon-playback-play", 159 | "icon-playback-rewind", 160 | "icon-plug", 161 | "icon-plus-small", 162 | "icon-plus", 163 | "icon-podium", 164 | "icon-primitive-dot", 165 | "icon-primitive-square", 166 | "icon-pulse", 167 | "icon-puzzle", 168 | "icon-question", 169 | "icon-quote", 170 | "icon-radio-tower", 171 | "icon-remove-close", 172 | "icon-reply", 173 | "icon-repo-clone", 174 | "icon-repo-create", 175 | "icon-repo-delete", 176 | "icon-repo-force-push", 177 | "icon-repo-forked", 178 | "icon-repo-pull", 179 | "icon-repo-push", 180 | "icon-repo-sync", 181 | "icon-repo", 182 | "icon-rocket", 183 | "icon-rss", 184 | "icon-ruby", 185 | "icon-screen-full", 186 | "icon-screen-normal", 187 | "icon-search-save", 188 | "icon-search", 189 | "icon-server", 190 | "icon-settings", 191 | "icon-shield", 192 | "icon-sign-in", 193 | "icon-sign-out", 194 | "icon-smiley", 195 | "icon-split", 196 | "icon-squirrel", 197 | "icon-star-add", 198 | "icon-star-delete", 199 | "icon-star", 200 | "icon-steps", 201 | "icon-stop", 202 | "icon-sync", 203 | "icon-tag-add", 204 | "icon-tag-remove", 205 | "icon-tag", 206 | "icon-tasklist", 207 | "icon-telescope", 208 | "icon-terminal", 209 | "icon-text-size", 210 | "icon-three-bars", 211 | "icon-thumbsdown", 212 | "icon-thumbsup", 213 | "icon-tools", 214 | "icon-trashcan", 215 | "icon-triangle-down", 216 | "icon-triangle-left", 217 | "icon-triangle-right", 218 | "icon-triangle-up", 219 | "icon-unfold", 220 | "icon-unmute", 221 | "icon-unverified", 222 | "icon-verified", 223 | "icon-versions", 224 | "icon-watch", 225 | "icon-x", 226 | "icon-zap" 227 | ] 228 | } 229 | -------------------------------------------------------------------------------- /src/json/release-notes.json: -------------------------------------------------------------------------------- 1 | { 2 | "v140": "## [1.4.0] - 2019-07-07\n\n### Fixed\n\n- Issue ([#213](https://github.com/jccguimaraes/atom-project-viewer/issues/213))." 3 | } 4 | -------------------------------------------------------------------------------- /src/main-view.js: -------------------------------------------------------------------------------- 1 | const map = require('./map'); 2 | const domBuilder = require('./dom-builder'); 3 | const api = require('./api'); 4 | const colours = require('./colours'); 5 | const database = require('./database'); 6 | const {getModel, sortList} = require('./common'); 7 | 8 | const viewsRef = {}; 9 | let startX; 10 | let startWidth; 11 | let dragListener; 12 | let stopListener; 13 | 14 | const invertResizer = function _invertResizer (inverted) { 15 | if (inverted) { 16 | viewsRef['resizer'].classList.add('invert'); 17 | } 18 | else { 19 | viewsRef['resizer'].classList.remove('invert'); 20 | } 21 | }; 22 | 23 | const resizerResetDrag = function _resizerResetDrag () { 24 | this.removeAttribute('style'); 25 | atom.config.set('project-viewer.customWidth', undefined); 26 | }; 27 | 28 | const resizerInitializeDrag = function _resizerInitializeDrag (event) { 29 | this.classList.add('resizing'); 30 | startX = event.clientX; 31 | startWidth = parseInt(window.getComputedStyle(this).width, 10); 32 | document.addEventListener('mousemove', dragListener, false); 33 | document.addEventListener('mouseup', stopListener, false); 34 | }; 35 | 36 | const resizerDoDrag = function _resizerDoDrag (event) { 37 | let variation; 38 | if (atom.config.get('project-viewer.panelPosition').includes('Left')) { 39 | variation = event.clientX - startX; 40 | } 41 | else { 42 | variation = startX - event.clientX; 43 | } 44 | this.setAttribute('style', `width:${startWidth + variation}px;`); 45 | }; 46 | 47 | const resizerStopDrag = function _resizerStopDrag () { 48 | this.classList.remove('resizing'); 49 | document.removeEventListener('mousemove', dragListener, false); 50 | document.removeEventListener('mouseup', stopListener, false); 51 | let value = parseInt(window.getComputedStyle(this).width, 10); 52 | if (value === 200) { value = undefined; } 53 | this.removeAttribute('style'); 54 | atom.config.set('project-viewer.customWidth', value); 55 | }; 56 | 57 | const buildViews = function _buildViews (model) { 58 | let view; 59 | if (model.type === 'group') { 60 | view = api.group.createView(model); 61 | } 62 | else if (model.type === 'project') { 63 | view = api.project.createView(model); 64 | } 65 | view.initialize(); 66 | view.render(); 67 | viewsRef[model.uuid] = view; 68 | 69 | const parentModel = Object.getPrototypeOf(model); 70 | if (parentModel === Object.prototype) { 71 | this.attachChild(view); 72 | } 73 | else if (viewsRef.hasOwnProperty(parentModel.uuid)) { 74 | viewsRef[parentModel.uuid].attachChild(view); 75 | } 76 | }; 77 | 78 | const toggleTitle = function _toggleTitle (visibility) { 79 | const title = this.querySelector('.heading'); 80 | if (!title) { return; } 81 | title.classList.toggle('hidden', visibility); 82 | }; 83 | 84 | const validateActivePaneItem = function _validateActivePaneItem (item) { 85 | return item.nodeName === 'PROJECT-VIEWER-EDITOR' && 86 | item.getAttribute('data-pv-uuid') === this.uuid; 87 | }; 88 | 89 | const isAlreadyEditing = function _isAlreadyEditing (model) { 90 | return model && atom.workspace.getActivePane().items.some( 91 | validateActivePaneItem, model 92 | ); 93 | }; 94 | 95 | const openEditor = function _openEditor (model, prefill) { 96 | if (isAlreadyEditing(model)) { return; } 97 | const activePane = atom.workspace.getCenter().getActivePane(); 98 | const editorItem = api.editor.createView(); 99 | editorItem.tabIndex = 0; 100 | editorItem.initialize(model, prefill); 101 | activePane.addItem(editorItem); 102 | activePane.activateItem(editorItem); 103 | }; 104 | 105 | const reset = function _reset () { 106 | const listTree = this.querySelector('ul.list-tree'); 107 | if (!listTree) { 108 | return; 109 | } 110 | while (listTree.firstChild) { 111 | listTree.removeChild(listTree.firstChild); 112 | } 113 | }; 114 | 115 | const populate = function _populate (list) { 116 | if (!list || !Array.isArray(list)) { 117 | return; 118 | } 119 | this.reset(); 120 | 121 | if (list.length === 0) { 122 | const emptyMessage = this.querySelector('.background-message'); 123 | emptyMessage.classList.remove('hidden'); 124 | return; 125 | } 126 | 127 | list.forEach(buildViews.bind(this)); 128 | this.sortChildren(); 129 | for (let ref in viewsRef) { 130 | const model = getModel(viewsRef[ref]); 131 | if (model && model.type === 'group') { 132 | viewsRef[ref].sortChildren(); 133 | } 134 | } 135 | }; 136 | 137 | const traverse = function _traverse (direction) { 138 | const selectionsUnfiltered = this.querySelectorAll( 139 | `li[is="project-viewer-group"], 140 | li[is="project-viewer-project"]` 141 | ); 142 | 143 | let selectionsFiltered = Array.from(selectionsUnfiltered).filter( 144 | selection => { 145 | let isVisible = true; 146 | let parent = selection.parentNode; 147 | if (!parent) { return; } 148 | while(!parent.classList.contains('body-content')) { 149 | parent = parent.parentNode; 150 | if (!parent || parent.classList.contains('collapsed')) { 151 | selection.classList.remove('active'); 152 | isVisible = false; 153 | break; 154 | } 155 | } 156 | return isVisible; 157 | } 158 | ); 159 | 160 | let nextIdx = 0; 161 | 162 | selectionsFiltered.some( 163 | (selection, idx) => { 164 | if (selection.classList.contains('active')) { 165 | selection.classList.remove('active'); 166 | nextIdx = direction === '☝️' ? idx - 1 : idx + 1; 167 | return true; 168 | } 169 | } 170 | ); 171 | 172 | if (direction === '☝️' && nextIdx === -1) { 173 | nextIdx = selectionsFiltered.length - 1; 174 | } 175 | else if ( 176 | (direction === '👇' && nextIdx === selectionsFiltered.length) || 177 | direction === undefined 178 | ) { 179 | nextIdx = 0; 180 | } 181 | if (!selectionsFiltered[nextIdx]) { return; } 182 | selectionsFiltered[nextIdx].classList.add('active'); 183 | }; 184 | 185 | const setAction = function _setAction (action) { 186 | const selectedView = this.querySelector( 187 | `li[is="project-viewer-group"].active, 188 | li[is="project-viewer-project"].active` 189 | ); 190 | 191 | if (!selectedView) { return false; } 192 | 193 | const model = map.get(selectedView); 194 | 195 | if (!model) { return false; } 196 | 197 | if (model.type === 'group' && action === '📪') { 198 | selectedView.classList.add('collapsed'); 199 | } 200 | else if (model.type === 'group' && action === '📭') { 201 | selectedView.classList.remove('collapsed'); 202 | } 203 | else if (model.type === 'project' && action === '✅') { 204 | if (atom.config.get('project-viewer.autoHide')) { 205 | this.toggleFocus(); 206 | } 207 | selectedView.openOnWorkspace(); 208 | } 209 | else if (model.type === 'group' && action === '✅') { 210 | selectedView.classList.toggle('collapsed'); 211 | } 212 | }; 213 | 214 | const autohide = function _autohide (option) { 215 | if (option) { 216 | this.classList.add('autohide'); 217 | } 218 | else if (option === false) { 219 | this.classList.remove('autohide'); 220 | } 221 | else { 222 | this.classList.toggle('autohide'); 223 | } 224 | }; 225 | 226 | const autoHideAbsolute = function _autoHideAbsolute (option) { 227 | if (option) { 228 | this.classList.add('position-absolute'); 229 | } 230 | else { 231 | this.classList.remove('position-absolute'); 232 | } 233 | 234 | if (option && atom.config.get('project-viewer.panelPosition').startsWith('Left')) { 235 | this.classList.remove('position-right'); 236 | this.classList.add('position-left'); 237 | } 238 | else if (option && atom.config.get('project-viewer.panelPosition').startsWith('Right')) { 239 | this.classList.add('position-right'); 240 | this.classList.remove('position-left'); 241 | } 242 | else { 243 | this.classList.remove('position-left', 'position-right'); 244 | } 245 | }; 246 | 247 | const toggleFocus = function _toggleFocus () { 248 | const panel = atom.workspace.panelForItem(this); 249 | if (!panel) { return false; } 250 | const item = panel.getItem(); 251 | if (!item) { return false; } 252 | 253 | if (document.activeElement === item) { 254 | if (atom.config.get('project-viewer.autoHide')) { 255 | item.classList.add('autohide'); 256 | } 257 | atom.workspace.getActivePane().activate(); 258 | const selectedView = this.querySelector( 259 | `li[is="project-viewer-project"].active, 260 | li[is="project-viewer-project"].active` 261 | ); 262 | if (selectedView) { 263 | selectedView.classList.remove('active'); 264 | } 265 | } else { 266 | if (atom.config.get('project-viewer.autoHide')) { 267 | item.classList.remove('autohide'); 268 | } 269 | const activeView = this.querySelector( 270 | 'li[is="project-viewer-project"].selected' 271 | ); 272 | if (activeView) { 273 | activeView.classList.add('active'); 274 | } 275 | item.focus(); 276 | } 277 | }; 278 | 279 | const viewUnfocus = function _viewUnfocus () { 280 | if (atom.config.get('project-viewer.autoHide')) { 281 | this.classList.add('autohide'); 282 | } 283 | const selectedView = this.querySelector( 284 | `li[is="project-viewer-group"].active, 285 | li[is="project-viewer-project"].active` 286 | ); 287 | if (!selectedView) { return; } 288 | 289 | selectedView.classList.remove('active'); 290 | } 291 | 292 | const initialize = function _initialize () { 293 | 294 | this.addEventListener('blur', viewUnfocus.bind(this)); 295 | 296 | this.setAttribute('tabindex', -1); 297 | this.classList.add('pv-has-icons'); 298 | 299 | let hiddenBlock = document.createElement('div'); 300 | hiddenBlock.classList.add('hidden-block'); 301 | 302 | let pvResizer = document.createElement('div'); 303 | pvResizer.classList.add('pv-resizer'); 304 | viewsRef['resizer'] = pvResizer; 305 | 306 | let customWidth = atom.config.get('project-viewer.customWidth'); 307 | 308 | if (customWidth !== 200) { 309 | // this.setAttribute('style', `width:${atom.config.get('project-viewer.customWidth')}px;`); 310 | colours.removeRule('app'); 311 | colours.addRule('app', 'app', customWidth); 312 | } 313 | 314 | let customHotZone = Math.min(atom.config.get('project-viewer.customHotZone'), customWidth); 315 | 316 | if (customHotZone !== 20) { 317 | colours.removeRule('hotzone'); 318 | colours.addRule('hotzone', 'hotzone', customHotZone); 319 | } 320 | 321 | let panelHeading = document.createElement('h2'); 322 | panelHeading.classList.add('heading'); 323 | panelHeading.textContent = 'Project-Viewer'; 324 | 325 | let panelBody = document.createElement('div'); 326 | panelBody.classList.add('body-content'); 327 | 328 | let emptyMessage = document.createElement('ul'); 329 | emptyMessage.classList.add('background-message', 'centered'); 330 | let emptyMessageText = document.createElement('li'); 331 | emptyMessageText.textContent = 'No groups or projects'; 332 | 333 | let listTree = document.createElement('ul'); 334 | listTree.classList.add( 335 | 'list-tree', 336 | 'has-collapsable-children', 337 | 'pv-has-custom-icons' 338 | ); 339 | 340 | this.addEventListener('dragstart', (evt) => { 341 | evt.stopPropagation(); 342 | }, false); 343 | 344 | this.addEventListener('dragover', (evt) => { 345 | evt.preventDefault(); 346 | }, false); 347 | 348 | this.addEventListener('dragleave', (evt) => { 349 | evt.stopPropagation(); 350 | }, false); 351 | 352 | this.addEventListener('dragenter', (evt) => { 353 | evt.stopPropagation(); 354 | }, false); 355 | 356 | this.addEventListener('dragend', (evt) => { 357 | evt.target.classList.remove('dragged'); 358 | evt.stopPropagation(); 359 | }, false); 360 | 361 | this.addEventListener('drop', (evt) => { 362 | const uuid = evt.dataTransfer.getData("text/plain"); 363 | const view = viewsRef[uuid]; 364 | if (!view) { return; } 365 | const draggedModel = getModel(view); 366 | database.moveTo(draggedModel); 367 | database.save(); 368 | }, false); 369 | 370 | emptyMessage.appendChild(emptyMessageText); 371 | panelBody.appendChild(listTree); 372 | panelBody.appendChild(emptyMessage); 373 | hiddenBlock.appendChild(panelHeading); 374 | hiddenBlock.appendChild(panelBody); 375 | hiddenBlock.appendChild(pvResizer); 376 | this.appendChild(hiddenBlock); 377 | 378 | dragListener = resizerDoDrag.bind(this); 379 | stopListener = resizerStopDrag.bind(this); 380 | pvResizer.addEventListener('mousedown', resizerInitializeDrag.bind(this), false); 381 | pvResizer.addEventListener("dblclick", resizerResetDrag.bind(this), false); 382 | }; 383 | 384 | const sortChildren = function _sortChildren () { 385 | const listTree = this.querySelector('.list-tree'); 386 | if (!listTree) { return; } 387 | const children = Array.from(listTree.children); 388 | if (!children || children.length === 0) { return; } 389 | 390 | sortList(children, atom.config.get('project-viewer.rootSortBy')); 391 | children.forEach(view => listTree.appendChild(view)); 392 | }; 393 | 394 | const attachChild = function _attachChild (node) { 395 | const listTree = this.querySelector('.list-tree'); 396 | 397 | if (!listTree) { return; } 398 | 399 | if (listTree.children.length === 0) { 400 | const emptyMessage = this.querySelector('.background-message'); 401 | emptyMessage.classList.add('hidden'); 402 | } 403 | 404 | listTree.appendChild(node); 405 | }; 406 | 407 | const detachChild = function _detachChild (node) { 408 | let listTree = this.querySelector('.list-tree'); 409 | if (!listTree) { 410 | return; 411 | } 412 | listTree.removeChild(node); 413 | }; 414 | 415 | const getTitle = function _getTitle () { 416 | return database.PACKAGE_NAME; 417 | }; 418 | 419 | const getURI = function _getURI () { 420 | return database.WORKSPACE_URI; 421 | }; 422 | 423 | const getDefaultLocation = function _getDefaultLocation () { 424 | return 'right'; 425 | }; 426 | 427 | const getAllowedLocations = function _getAllowedLocations () { 428 | return ['left', 'right']; 429 | }; 430 | 431 | const isPermanentDockItem = function _isPermanentDockItem () { 432 | return true; 433 | }; 434 | 435 | const viewMethods = { 436 | attachChild, 437 | sortChildren, 438 | detachChild, 439 | autohide, 440 | autoHideAbsolute, 441 | initialize, 442 | openEditor, 443 | populate, 444 | reset, 445 | setAction, 446 | toggleFocus, 447 | toggleTitle, 448 | traverse, 449 | invertResizer, 450 | getTitle, 451 | getURI, 452 | getDefaultLocation, 453 | getAllowedLocations, 454 | isPermanentDockItem, 455 | getPreferredWidth: () => { 456 | if (this.list && this.list.style) { 457 | this.list.style.width = '200px'; 458 | } 459 | } 460 | }; 461 | 462 | const createView = function _createView (model) { 463 | let options = { 464 | tagIs: 'project-viewer' 465 | }; 466 | return domBuilder.createView(options, viewMethods, model); 467 | }; 468 | 469 | module.exports = { 470 | createView: createView 471 | }; 472 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | const map = new WeakMap(); 2 | 3 | module.exports = map; 4 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | 3 | const defaults = { 4 | name: 'unnamed', 5 | sortBy: 'position', 6 | icon: '', 7 | color: '', 8 | expanded: false, 9 | devMode: false, 10 | config: {} 11 | }; 12 | 13 | const groupModel = { 14 | type: 'group', 15 | name: defaults.name, 16 | sortBy: defaults.sortBy, 17 | icon: defaults.icon, 18 | color: defaults.color, 19 | expanded: defaults.expanded 20 | }; 21 | 22 | const projectModel = { 23 | type: 'project', 24 | name: defaults.name, 25 | icon: defaults.icon, 26 | color: defaults.color, 27 | devMode: defaults.devMode, 28 | config: defaults.config 29 | }; 30 | 31 | const methods = { 32 | breadcrumb: function _breadcrumb () { 33 | let proto = Object.getPrototypeOf(this); 34 | let protoName = ''; 35 | if (proto !== Object.prototype) { 36 | protoName = proto.breadcrumb(); 37 | } 38 | return protoName.length === 0 ? this.name : `${protoName} / ${this.name}`; 39 | } 40 | }; 41 | 42 | const groupMethods = {}; 43 | 44 | const projectMethods = { 45 | clearPaths: function _clearPaths () { 46 | const removedPaths = this.paths.filter( 47 | () => true 48 | ); 49 | this.paths.length = 0; 50 | return removedPaths; 51 | }, 52 | addPaths: function _addPaths (paths) { 53 | if (!paths) { 54 | return; 55 | } 56 | if (Array.isArray(paths)) { 57 | paths.forEach( 58 | (path) => this.addPaths(path) 59 | ); 60 | return; 61 | } 62 | if (typeof paths !== 'string') { 63 | return; 64 | } 65 | const normalizedPath = Path.normalize(paths); 66 | if (this.paths.indexOf(normalizedPath) === -1) { 67 | this.paths.push(normalizedPath); 68 | } 69 | }, 70 | removePath: function _removePath (path) { 71 | const idx = this.paths.indexOf(path); 72 | if (idx === -1) { 73 | return; 74 | } 75 | this.paths.splice(idx, 1); 76 | }, 77 | removePaths: function _removePaths (paths) { 78 | let idxsToRemove = []; 79 | this.paths.forEach( 80 | (path, idx) => { 81 | if (paths.indexOf(path) !== -1) { 82 | idxsToRemove.push(idx); 83 | } 84 | } 85 | ); 86 | idxsToRemove.sort().reverse().forEach( 87 | (idxToRemove) => this.paths.splice(idxToRemove, 1) 88 | ); 89 | } 90 | }; 91 | 92 | Object.assign(groupMethods, methods); 93 | Object.assign(projectMethods, methods); 94 | 95 | const setPrototypeOf = function _setPrototypeOf (target, prototype) { 96 | if ( 97 | prototype === Object.prototype || 98 | (target.type === 'group' && target.type === prototype.type) || 99 | (prototype.type === 'group' && target.type === 'project') 100 | ) { 101 | Object.setPrototypeOf(target, prototype); 102 | return true; 103 | } 104 | return false; 105 | }; 106 | 107 | const handler = { 108 | setPrototypeOf: setPrototypeOf, 109 | get: function _get (target, property) { 110 | if (target.hasOwnProperty(property)) { 111 | return target[property]; 112 | } 113 | if (typeof target[property] === 'function') { 114 | return target[property]; 115 | } 116 | return null; 117 | }, 118 | set: function _set (target, property, value) { 119 | const allowedProps = [ 120 | 'name', 121 | 'sortBy', 122 | 'expanded', 123 | 'icon', 124 | 'color', 125 | 'devMode', 126 | 'config' 127 | ]; 128 | if (allowedProps.indexOf(property) === -1) { 129 | return true; 130 | } 131 | let cleanValue; 132 | 133 | if (value === undefined) { 134 | target[property] = defaults[property]; 135 | return true; 136 | } 137 | if (property === 'name') { 138 | // TODO: make this in a helper function? 139 | const UNSAFE_CHARS_PATTERN = /[<>\/\u2028\u2029]/g; 140 | const UNICODE_CHARS = { 141 | '<': '\\u003C', 142 | '>': '\\u003E', 143 | '/': '\\u002F', 144 | '\u2028': '\\u2028', 145 | '\u2029': '\\u2029' 146 | }; 147 | cleanValue = value.length > 0 && value.replace && value.replace( 148 | UNSAFE_CHARS_PATTERN, 149 | function (unsafeChar) { return UNICODE_CHARS[unsafeChar]; } 150 | ) === value ? value : target[property]; 151 | } 152 | else if (target.type === 'group' && property === 'sortBy') { 153 | const allowed = [ 154 | 'position', 155 | 'reverse-position', 156 | 'alphabetically', 157 | 'reverse-alphabetically' 158 | ]; 159 | cleanValue = allowed.indexOf(value) !== -1 ? value : target[property]; 160 | } 161 | else if (target.type === 'group' && property === 'expanded') { 162 | cleanValue = Boolean(value) === value ? value : target[property]; 163 | } 164 | else if (property === 'icon') { 165 | const allowed = [ 166 | 'icon-', 167 | 'devicons-' 168 | ]; 169 | cleanValue = allowed.map( 170 | (val) => value && value.startsWith(val) ? value : undefined 171 | ).filter( 172 | (val) => val !== undefined 173 | ); 174 | 175 | cleanValue = cleanValue.length === 1 ? cleanValue[0] : target[property]; 176 | } 177 | else if (property === 'color') { 178 | const regEx = new RegExp('^#(?:[0-9a-f]{3}){1,2}$', 'i'); 179 | cleanValue = regEx.exec(value) !== null ? value : target[property]; 180 | } 181 | else if (target.type === 'project' && property === 'devMode') { 182 | cleanValue = Boolean(value) === value ? value : target[property]; 183 | } 184 | else if (target.type === 'project' && property === 'config') { 185 | cleanValue = target[property]; 186 | } 187 | target[property] = cleanValue; 188 | return true; 189 | } 190 | }; 191 | 192 | module.exports = { 193 | createGroup: function _createGroup (candidate) { 194 | const group = Object.assign(groupModel); 195 | const model = Object.assign({}, group, groupMethods); 196 | model.uuid = 'pv_' + Math.ceil(Date.now() * Math.random()); 197 | const proxy = new Proxy(model, handler); 198 | if (candidate) { 199 | Object.assign(proxy, candidate); 200 | } 201 | return proxy; 202 | }, 203 | createProject: function _createproject (candidate) { 204 | const project = Object.assign(projectModel); 205 | project.paths = [] 206 | const model = Object.assign({}, project, projectMethods); 207 | model.uuid = 'pv_' + Math.ceil(Date.now() * Math.random()); 208 | const proxy = new Proxy(model, handler); 209 | if (candidate) { 210 | Object.assign(proxy, candidate); 211 | proxy.addPaths(candidate.paths); 212 | } 213 | return proxy; 214 | }, 215 | createGroupSchema: function _createGroupSchema ( 216 | { 217 | type = 'group', 218 | name = groupModel.name === defaults.name ? '' : groupModel.name, 219 | sortBy = groupModel.sortBy, 220 | icon = groupModel.icon, 221 | color = groupModel.color, 222 | expanded = groupModel.expanded 223 | } = {} 224 | ) { 225 | return { type, name, sortBy, icon, color, expanded }; 226 | }, 227 | createProjectSchema: function _createProjectSchema ( 228 | { 229 | type = 'project', 230 | name = projectModel.name === defaults.name ? '' : projectModel.name, 231 | icon = projectModel.icon, 232 | color = projectModel.color, 233 | devMode = projectModel.devMode, 234 | config = projectModel.config, 235 | paths = [] 236 | } = {} 237 | ) { 238 | return { type, name, icon, color, devMode, config, paths }; 239 | } 240 | }; 241 | -------------------------------------------------------------------------------- /src/packages.js: -------------------------------------------------------------------------------- 1 | const showPackage = function _showPackage (name, pkg) { 2 | if (name === 'tree-view') { 3 | pkg.mainModule.treeView.show(); 4 | } 5 | }; 6 | 7 | /** 8 | * Enables / activates a list of packages 9 | * 10 | * @param {Array} list an array of strings with all the packages name 11 | * @returns {void} 12 | */ 13 | const enablePackages = function _enablePackages (list) { 14 | list.forEach(pkg => { 15 | if (!pkg || pkg.trim().length === 0) { return; } 16 | const enabledPackage = atom.packages.enablePackage(pkg); 17 | showPackage(pkg, enabledPackage); 18 | }); 19 | }; 20 | 21 | const disablePackages = function _disablePackages (list) { 22 | list.forEach(pkg => { 23 | if (!pkg || pkg.trim().length === 0) { return; } 24 | atom.packages.disablePackage(pkg); 25 | }); 26 | }; 27 | 28 | const getTreeViewState = function _getTreeViewState () { 29 | const treeView = atom.packages.getActivePackage('tree-view'); 30 | if (!treeView || !treeView.mainModule || !treeView.mainModule.treeView) { 31 | return {}; 32 | } 33 | return treeView.mainModule.treeView.serialize(); 34 | }; 35 | 36 | const setTreeViewState = function _setTreeViewState (state) { 37 | if (!state) { return; } 38 | const pkg = atom.packages.getActivePackage('tree-view'); 39 | if (!pkg || !pkg.mainModule || !pkg.mainModule.treeView) { 40 | return; 41 | } 42 | 43 | if (!pkg.mainModule.treeView) { 44 | pkg.mainModule.createView(state.directoryExpansionStates); 45 | } else { 46 | pkg.mainModule.treeView.updateRoots(state.directoryExpansionStates); 47 | } 48 | 49 | const element = pkg.mainModule.treeView.element; 50 | 51 | if (state.width > 0) { 52 | element.style.width = `${state.width}px`; 53 | } 54 | 55 | element.scrollTop = state.scrollTop; 56 | element.scrollLeft = state.scrollLeft; 57 | } 58 | 59 | module.exports = { 60 | treeView: { 61 | getState: getTreeViewState, 62 | setState: setTreeViewState 63 | }, 64 | state: { 65 | enable: enablePackages, 66 | disable: disablePackages 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/project-view.js: -------------------------------------------------------------------------------- 1 | const { 2 | getCurrentOpenedProject, getModel, getViewFromModel, 3 | getSelectedProject, getView 4 | } = require('./common'); 5 | const colours = require('./colours'); 6 | const database = require('./database'); 7 | const domBuilder = require('./dom-builder'); 8 | const map = require('./map'); 9 | const remote = require('remote'); 10 | const statusBar = require('./status-bar'); 11 | const packages = require('./packages'); 12 | 13 | let checkedAll = []; 14 | 15 | const onClickEvent = function _onClickEvent (model) { 16 | if (!model) { return null; } 17 | this.openOnWorkspace(); 18 | }; 19 | 20 | const dragstart = function _dragstart (evt) { 21 | const view = getView(evt.target); 22 | const model = getModel(evt.target); 23 | view.classList.add('dragging'); 24 | evt.dataTransfer.setData('text/plain', model.uuid); 25 | evt.stopPropagation(); 26 | }; 27 | 28 | const dragover = function _dragover (evt) { 29 | const view = getView(evt.target); 30 | const middle = view.clientHeight / 2; 31 | 32 | if (evt.target.offsetTop + middle > evt.layerY) { 33 | evt.target.classList.add('above'); 34 | evt.target.classList.remove('below'); 35 | } 36 | else { 37 | evt.target.classList.remove('above'); 38 | evt.target.classList.add('below'); 39 | } 40 | 41 | evt.preventDefault(); 42 | evt.stopPropagation(); 43 | }; 44 | 45 | const dragleave = function _dragleave (evt) { 46 | evt.preventDefault(); 47 | this.classList.remove('dropping', 'below', 'above'); 48 | }; 49 | 50 | const dragenter = function _dragenter (evt) { 51 | evt.preventDefault(); 52 | const uuid = evt.dataTransfer.getData('text/plain'); 53 | const draggedView = document.querySelector( 54 | `project-viewer li[data-project-viewer-uuid="${uuid}"]` 55 | ); 56 | const view = getView(evt.target); 57 | if (view === draggedView) { return; } 58 | 59 | this.classList.add('dropping'); 60 | }; 61 | 62 | const dragend = function _dragend (evt) { 63 | const view = getView(evt.target); 64 | view.classList.remove('dragging'); 65 | evt.stopPropagation(); 66 | }; 67 | 68 | const drop = function _drop (evt) { 69 | evt.stopPropagation(); 70 | 71 | const uuid = evt.dataTransfer.getData('text/plain'); 72 | 73 | const draggedView = document.querySelector( 74 | `project-viewer li[data-project-viewer-uuid="${uuid}"]` 75 | ); 76 | const droppedView = getView(evt.target); 77 | 78 | if (droppedView === draggedView) { return; } 79 | 80 | const droppedModel = getModel(evt.target); 81 | const draggedModel = getModel(draggedView); 82 | 83 | // a bit hacky 84 | const insertBefore = droppedView.classList.contains('above'); 85 | this.classList.remove('dropping', 'below', 'above'); 86 | 87 | database.moveTo(draggedModel, droppedModel, insertBefore); 88 | database.save(); 89 | }; 90 | 91 | const attachedCallback = function _attachedCallback () { 92 | this.addEventListener('dragstart', dragstart, false); 93 | this.addEventListener('dragover', dragover, false); 94 | this.addEventListener('dragleave', dragleave, false); 95 | this.addEventListener('dragenter', dragenter, false); 96 | this.addEventListener('dragend', dragend, false); 97 | this.addEventListener('drop', drop, false); 98 | }; 99 | 100 | const detachedCallback = function _detachedCallback () { 101 | this.removeEventListener('dragstart', dragstart, false); 102 | this.removeEventListener('dragover', dragover, false); 103 | this.removeEventListener('dragleave', dragleave, false); 104 | this.removeEventListener('dragenter', dragenter, false); 105 | this.removeEventListener('dragend', dragend, false); 106 | this.removeEventListener('drop', drop, false); 107 | }; 108 | 109 | const initialize = function _initialize () { 110 | const model = map.get(this); 111 | 112 | if (!model) { return; } 113 | 114 | this.classList.add('list-item'); 115 | 116 | this.setAttribute('data-project-viewer-uuid', model.uuid); 117 | this.setAttribute('draggable', 'true'); 118 | 119 | this.addEventListener( 120 | 'click', 121 | onClickEvent.bind(this, model) 122 | ); 123 | }; 124 | 125 | const render = function _render () { 126 | const model = map.get(this); 127 | 128 | if (!model) { 129 | return; 130 | } 131 | 132 | let spanNode = this.querySelector('span'); 133 | let contentNode = this; 134 | 135 | if (!spanNode) { 136 | contentNode.textContent = ''; 137 | spanNode = document.createElement('span'); 138 | contentNode.appendChild(spanNode); 139 | } 140 | 141 | if (model.icon) { 142 | contentNode = spanNode; 143 | if (model.icon.startsWith('devicons-')) { 144 | contentNode.classList.add('devicons', model.icon); 145 | } 146 | else { 147 | contentNode.classList.add('icon', model.icon); 148 | } 149 | } 150 | else if (spanNode) { 151 | contentNode = spanNode; 152 | } 153 | 154 | if (model.name) { 155 | contentNode.textContent = model.name; 156 | } 157 | 158 | if (model.color) { 159 | colours.addRule(model.uuid, model.type, model.color); 160 | } else { 161 | colours.removeRule(model.uuid); 162 | } 163 | 164 | this.classList.toggle('no-paths', model.paths.length === 0); 165 | 166 | const currentOpenedProject = getCurrentOpenedProject(model); 167 | 168 | this.classList.toggle( 169 | 'selected', 170 | currentOpenedProject 171 | ); 172 | 173 | if (currentOpenedProject) { 174 | statusBar.update(model.breadcrumb()); 175 | } 176 | }; 177 | 178 | const sorting = function _sorting () { 179 | const model = map.get(this); 180 | 181 | if (!model) { 182 | return; 183 | } 184 | return model.name; 185 | }; 186 | 187 | const checkIfOpened = function _checkIfOpened (event, model, title, opened, action) { 188 | const wcs = remote.webContents; 189 | 190 | checkedAll.push({ 191 | title, 192 | opened 193 | }); 194 | 195 | if (wcs.length !== checkedAll.length) { 196 | if (action) { 197 | atom.open({ 198 | pathsToOpen: model.paths, 199 | // newWindow: true, 200 | devMode: model.devMode, 201 | safeMode: false 202 | }); 203 | } 204 | return; 205 | } 206 | 207 | remote.ipcMain.removeListener(`channel-${model.uuid}`, checkIfOpened); 208 | 209 | const openNew = checkedAll.find(checked => checked.opened); 210 | checkedAll = []; 211 | 212 | if (!openNew) { 213 | atom.open({ 214 | pathsToOpen: model.paths, 215 | // newWindow: true, 216 | devMode: model.devMode, 217 | safeMode: false 218 | }); 219 | return; 220 | } 221 | 222 | wcs.some(wc => { 223 | if (wc.getTitle() === openNew.title) { 224 | wc.focus(); 225 | return true; 226 | } 227 | }); 228 | }; 229 | 230 | const openOnWorkspace = async function _openOnWorkspace (reverseOption) { 231 | const model = map.get(this); 232 | 233 | if (!model) { return false; } 234 | 235 | if (model.paths.length === 0) { return false; } 236 | 237 | const selectedProject = getSelectedProject(); 238 | 239 | if (selectedProject === getViewFromModel(model)) { return; } 240 | 241 | const action = reverseOption ? 242 | !atom.config.get('project-viewer.openNewWindow') : 243 | atom.config.get('project-viewer.openNewWindow'); 244 | 245 | if (action) { 246 | remote.ipcMain.on(`channel-${model.uuid}`, checkIfOpened); 247 | remote.webContents.getAllWebContents().forEach(wc => { 248 | if (!wc.browserWindowOptions) { return; } 249 | wc.webContents.send( 250 | 'pv-check-if-opened', 251 | model, wc.getTitle(), 252 | action 253 | ); 254 | }); 255 | return false; 256 | } 257 | 258 | const packagesList = atom.config.get('project-viewer.packagesReload'); 259 | 260 | if (!atom.config.get('project-viewer.openNewWindow')) { 261 | packages.state.disable(packagesList); 262 | } 263 | 264 | if (selectedProject) { 265 | selectedProject.classList.remove('selected'); 266 | } 267 | 268 | this.classList.add('selected'); 269 | 270 | let projectSHA; 271 | let serialization; 272 | 273 | projectSHA = atom.getStateKey(atom.project.getPaths()); 274 | 275 | // We need to ensure that the projectSHA is valid (i.e. the project contains at least one path) 276 | if (projectSHA !== null) { 277 | if (atom.config.get('project-viewer.keepContext') || database.KEEP_CONTEXT) { 278 | database.KEEP_CONTEXT = false; 279 | serialization = await atom.stateStore.load(projectSHA); 280 | 281 | // Ensure that the serialized file exists 282 | if (typeof serialization === 'undefined') { 283 | serialization = atom.serialize(); 284 | } 285 | } else { 286 | serialization = atom.serialize(); 287 | } 288 | 289 | serialization.treeView = packages.treeView.getState(); 290 | await atom.stateStore.save(projectSHA, serialization); 291 | } 292 | 293 | statusBar.update(model.breadcrumb()); 294 | 295 | projectSHA = atom.getStateKey(model.paths); 296 | 297 | const state = await atom.stateStore.load(projectSHA); 298 | 299 | database.pathsChangedBypass = true; 300 | 301 | if (!state || !state.workspace.paneContainers) { 302 | atom.project.setPaths(model.paths); 303 | atom.workspace.getCenter().paneContainer.activePane.destroy(); 304 | } 305 | else { 306 | if (state.workspace.paneContainers && state.workspace.paneContainers.left) { 307 | state.workspace.paneContainers.left.paneContainer = {}; 308 | } 309 | if (state.workspace.paneContainers && state.workspace.paneContainers.left) { 310 | state.workspace.paneContainers.right.paneContainer = {}; 311 | } 312 | if (state.workspace.paneContainers && state.workspace.paneContainers.left) { 313 | state.workspace.paneContainers.bottom.paneContainer = {}; 314 | } 315 | 316 | if (atom.config.get('project-viewer.keepContext')) { 317 | state.workspace.paneContainers.center = {}; 318 | } 319 | if (atom.config.get('project-viewer.keepWindowSize')) { 320 | state.windowDimensions = atom.getWindowDimensions(); 321 | state.fullScreen = atom.isFullScreen(); 322 | } 323 | 324 | atom.deserialize(state); 325 | // atom.restoreStateIntoThisEnvironment(state); 326 | 327 | packages.treeView.setState(state.treeView); 328 | if (!atom.config.get('project-viewer.openNewWindow')) { 329 | packages.state.enable(packagesList); 330 | } 331 | } 332 | 333 | 334 | 335 | database.pathsChangedBypass = false; 336 | 337 | return true; 338 | }; 339 | 340 | const viewMethods = { 341 | attachedCallback, 342 | detachedCallback, 343 | initialize, 344 | render, 345 | sorting, 346 | openOnWorkspace 347 | }; 348 | 349 | const createView = function _createView (model) { 350 | let options = { 351 | tagExtends: 'li', 352 | tagIs: 'project-viewer-project' 353 | }; 354 | if (!model) { 355 | return; 356 | } 357 | return domBuilder.createView(options, viewMethods, model); 358 | }; 359 | 360 | module.exports = { 361 | createView: createView 362 | }; 363 | -------------------------------------------------------------------------------- /src/projects-list-view.js: -------------------------------------------------------------------------------- 1 | const {CompositeDisposable} = require('atom'); 2 | const SelectList = require('atom-select-list'); 3 | 4 | class SelectListView { 5 | 6 | constructor () { 7 | this.items = []; 8 | this.selectListView = new SelectList({ 9 | items: this.items, 10 | emptyMessage: 'There are no projects...', 11 | didCancelSelection: () => this.cancel(), 12 | didConfirmEmptySelection: () => this.confirm(), 13 | didConfirmSelection: item => this.confirmSelection(item), 14 | filterKeyForItem: item => item.breadcrumb(), 15 | elementForItem: item => this.createItem(item) 16 | }); 17 | 18 | this.subscriptions = new CompositeDisposable() 19 | } 20 | 21 | get element () { 22 | return this.selectListView.element 23 | } 24 | 25 | destroy () { 26 | if (this.panel) { 27 | this.panel.destroy() 28 | } 29 | 30 | if (this.subscriptions) { 31 | this.subscriptions.dispose() 32 | this.subscriptions = null 33 | } 34 | 35 | return this.selectListView.destroy() 36 | } 37 | 38 | cancel () { 39 | this.selectListView.reset() 40 | this.hide() 41 | } 42 | 43 | confirm (item) { 44 | this.cancel(); 45 | } 46 | 47 | show () { 48 | this.previouslyFocusedElement = document.activeElement 49 | if (!this.panel) { 50 | this.panel = atom.workspace.addModalPanel({item: this}) 51 | } 52 | this.panel.show() 53 | this.selectListView.focus() 54 | } 55 | 56 | hide () { 57 | if (this.panel) { 58 | this.panel.hide() 59 | } 60 | 61 | if (this.previouslyFocusedElement) { 62 | this.previouslyFocusedElement.focus() 63 | this.previouslyFocusedElement = null 64 | } 65 | } 66 | 67 | setItems (items) { 68 | this.selectListView.update({items, loadingMessage: null, loadingBadge: null}); 69 | } 70 | } 71 | 72 | module.exports = SelectListView; 73 | -------------------------------------------------------------------------------- /src/projects-list.js: -------------------------------------------------------------------------------- 1 | const ProjectsListView = require('./projects-list-view'); 2 | const {getViewFromModel} = require('./common'); 3 | 4 | class ProjectsList extends ProjectsListView { 5 | toggle () { 6 | if (this.panel && this.panel.isVisible()) { 7 | this.cancel() 8 | } else { 9 | this.show() 10 | } 11 | } 12 | 13 | createItem (item) { 14 | const element = document.createElement('li'); 15 | element.className = 'item'; 16 | element.textContent = item.breadcrumb(); 17 | return element; 18 | } 19 | 20 | update (items) { 21 | this.setItems(items); 22 | } 23 | 24 | confirmSelection (item) { 25 | const view = getViewFromModel(item); 26 | if (view) { 27 | view.openOnWorkspace(); 28 | } 29 | this.cancel(); 30 | } 31 | } 32 | 33 | module.exports = ProjectsList; 34 | -------------------------------------------------------------------------------- /src/status-bar.js: -------------------------------------------------------------------------------- 1 | const map = require('./map'); 2 | 3 | const update = function _update (text) { 4 | if (!text) { return; } 5 | if (!statusBar.view || typeof statusBar.view.getItem !== 'function') { 6 | return; 7 | } 8 | const item = statusBar.view.getItem(); 9 | if (!item) { return; } 10 | item.textContent = text; 11 | }; 12 | 13 | const toggle = function _toggle (value) { 14 | const service = map.get(this); 15 | let item; 16 | if (!statusBar.view) { 17 | item = document.createElement('div'); 18 | item.classList.add('inline-block', 'pv-status-bar'); 19 | statusBar.view = service.addRightTile({ item }); 20 | } 21 | 22 | statusBar.view.destroy(); 23 | 24 | if (value) { 25 | statusBar.view = service.addRightTile({ 26 | item: statusBar.view.getItem() 27 | }); 28 | } 29 | }; 30 | 31 | const statusBar = Object.create(null); 32 | 33 | statusBar.update = update; 34 | statusBar.toggle = toggle; 35 | 36 | module.exports = statusBar; 37 | -------------------------------------------------------------------------------- /src/workers/github.js: -------------------------------------------------------------------------------- 1 | const api = { 2 | url: 'https://api.github.com/gists', 3 | gistDescription: 'atom.io project-viewer backup files', 4 | token: undefined, 5 | gistId: undefined, 6 | setName: 'default', 7 | get gistFileName() { 8 | return `project-viewer-${this.setName}.json`; 9 | }, 10 | oldBackupFileName: 'project-viewer.json', 11 | connectionError: function connectionError(error) { 12 | return { 13 | type: 'addError', 14 | message: `Failed to connect to GitHub servers: ${error.message}`, 15 | options: { 16 | icon: 'mark-github' 17 | } 18 | }; 19 | }, 20 | // helper function to check whether given configuration value is defined 21 | checkConfig: function checkConfig(value, name) { 22 | const promise = new Promise((resolve, reject) => { 23 | 24 | // check if given value is defined 25 | if (value) { 26 | resolve(); 27 | return; 28 | } 29 | 30 | reject({ 31 | type: 'addWarning', 32 | message: `No ${name} was provided, please check the configuration.`, 33 | options: { 34 | icon: 'mark-github' 35 | } 36 | }); 37 | }); 38 | 39 | return promise; 40 | }, 41 | getGist: function getGist() { 42 | const promise = new Promise((resolve, reject) => { 43 | let url = this.url + '/' + this.gistId; 44 | 45 | let headers = new Headers(); 46 | headers.append('Accept', 'application/vnd.github.v3+json'); 47 | headers.append('Authorization', `token ${this.token}`); 48 | 49 | let parameters = { 50 | method: 'GET', 51 | headers: headers 52 | }; 53 | 54 | fetch(url, parameters) 55 | .then(this.toJson.bind(null, 200)) 56 | .then(data => { 57 | if (data && data.files && (data.files.hasOwnProperty(this.gistFileName) || data.files.hasOwnProperty(this.oldBackupFileName))) { 58 | let fileName; 59 | let rename = false; 60 | // if new backup file exists select it for import 61 | if (data.files.hasOwnProperty(this.gistFileName)) { 62 | fileName = this.gistFileName; 63 | } else if (data.files.hasOwnProperty(this.oldBackupFileName)) { 64 | // otherwise fallback to old backup file and queue rename operation 65 | fileName = this.oldBackupFileName; 66 | rename = true; 67 | } 68 | 69 | // backup found, returning 70 | resolve({ 71 | type: 'addSuccess', 72 | message: 'Retrieved DB from GitHub successfully.', 73 | db: JSON.parse(data.files[fileName].content), 74 | options: { 75 | icon: 'mark-github' 76 | } 77 | }); 78 | 79 | if (rename) { 80 | // rename the file to conform with new backup system 81 | this.renameBackupFile(this.oldBackupFileName, this.gistFileName).then(postMessage).catch(postMessage); 82 | } 83 | return; 84 | } 85 | 86 | // backup not found, response status code was not 200 OK 87 | // either gist with given ID doesnt exist (user hasnt created the gist yet or it has been deleted) or user has no existing backup 88 | reject({ 89 | type: 'addWarning', 90 | message: `No backup found under gist ID [${this.gistId}] for set [${this.setName}]. Make sure that gist with given ID exists under your private gists and that you have an existing backup (call backup -> call import).`, 91 | options: { 92 | icon: 'mark-github', 93 | dismissable: true 94 | } 95 | }); 96 | }).catch(error => { 97 | reject(this.connectionError(error)); 98 | }); 99 | }); 100 | 101 | return promise; 102 | }, 103 | renameBackupFile: function renameBackupFile(oldValue, newValue) { 104 | const promise = new Promise((resolve, reject) => { 105 | let url = this.url + '/' + this.gistId; 106 | 107 | let headers = new Headers(); 108 | headers.append('Accept', 'application/vnd.github.v3+json'); 109 | headers.append('Authorization', `token ${this.token}`); 110 | 111 | let files = {}; 112 | files[oldValue] = { 113 | filename: newValue 114 | }; 115 | 116 | let body = JSON.stringify({ 117 | description: this.gistDescription, 118 | public: false, 119 | files: files 120 | }); 121 | 122 | parameters = { 123 | method: 'PATCH', 124 | headers: headers, 125 | body: body 126 | }; 127 | 128 | fetch(url, parameters) 129 | .then((response) => { 130 | if (response.status == 200) { 131 | // gist file successfully renamed 132 | resolve({ 133 | type: 'addSuccess', 134 | message: 'Successfully converted old backup file to a new one.', 135 | options: { 136 | icon: 'mark-github' 137 | } 138 | }); 139 | return; 140 | } 141 | 142 | // failed to update gist, reponse status code was not 200 OK 143 | reject({ 144 | type: 'addWarning', 145 | message: 'Failed to convert old backup file to a new one.', 146 | options: { 147 | icon: 'mark-github' 148 | } 149 | }); 150 | }).catch(error => { 151 | reject(this.connectionError(error)); 152 | }); 153 | }); 154 | 155 | return promise; 156 | }, 157 | updateGist: function updateGist(value, currentFiles) { 158 | const promise = new Promise((resolve, reject) => { 159 | let url = this.url + '/' + this.gistId; 160 | 161 | let headers = new Headers(); 162 | headers.append('Accept', 'application/vnd.github.v3+json'); 163 | headers.append('Authorization', `token ${this.token}`); 164 | 165 | let files = {}; 166 | files[this.gistFileName] = { 167 | content: JSON.stringify(value) 168 | }; 169 | 170 | if (currentFiles && currentFiles.hasOwnProperty(this.oldBackupFileName)) { 171 | // found old backup file, it will be deleted 172 | files[this.oldBackupFileName] = null; 173 | } 174 | 175 | let body = JSON.stringify({ 176 | description: this.gistDescription, 177 | public: false, 178 | files: files 179 | }); 180 | 181 | parameters = { 182 | method: 'PATCH', 183 | headers: headers, 184 | body: body 185 | }; 186 | 187 | fetch(url, parameters) 188 | .then((response) => { 189 | if (response.status == 200) { 190 | // gist successfully updated 191 | resolve({ 192 | type: 'addSuccess', 193 | message: 'Successfully backed up the DB.', 194 | options: { 195 | icon: 'mark-github' 196 | } 197 | }); 198 | return; 199 | } 200 | 201 | // failed to update gist, reponse status code was not 200 OK 202 | reject({ 203 | type: 'addWarning', 204 | message: 'Failed to update gist.', 205 | options: { 206 | icon: 'mark-github' 207 | } 208 | }); 209 | }).catch(error => { 210 | reject(this.connectionError(error)); 211 | }); 212 | }); 213 | 214 | return promise; 215 | }, 216 | checkIfGistExists: function checkIfGistExists() { 217 | const promise = new Promise((resolve, reject) => { 218 | let url = this.url + '/' + this.gistId; 219 | 220 | let headers = new Headers(); 221 | headers.append('Accept', 'application/vnd.github.v3+json'); 222 | headers.append('Authorization', `token ${this.token}`); 223 | 224 | let parameters = { 225 | method: 'GET', 226 | headers: headers, 227 | }; 228 | 229 | fetch(url, parameters) 230 | .then(this.toJson.bind(null, 200)) 231 | .then(data => { 232 | if (data) { 233 | // gist exists 234 | resolve(data); 235 | return; 236 | } 237 | 238 | // gist doesn't exist 239 | reject({ 240 | type: 'addWarning', 241 | message: `No gist found with ID [${this.gistId}]. Specify valid gist ID or specify empty gist ID and we will create a gist for you.`, 242 | options: { 243 | icon: 'mark-github', 244 | dismissable: true 245 | } 246 | }); 247 | }).catch(error => { 248 | reject(this.connectionError(error)); 249 | }); 250 | }); 251 | 252 | return promise; 253 | }, 254 | createNewGist: function createNewGist(value) { 255 | const promise = new Promise((resolve, reject) => { 256 | let headers = new Headers(); 257 | headers.append('Accept', 'application/vnd.github.v3+json'); 258 | headers.append('Authorization', `token ${this.token}`); 259 | 260 | let files = {}; 261 | files[this.gistFileName] = { 262 | content: JSON.stringify(value) 263 | }; 264 | 265 | let body = JSON.stringify({ 266 | description: this.gistDescription, 267 | public: false, 268 | files: files 269 | }); 270 | 271 | let parameters = { 272 | method: 'POST', 273 | headers: headers, 274 | body: body 275 | }; 276 | 277 | fetch(this.url, parameters) 278 | .then(this.toJson.bind(null, 201)) 279 | .then(data => { 280 | if (data && data.id) { 281 | // gist successfully created 282 | this.gistId = data.id; 283 | 284 | resolve({ 285 | type: 'addSuccess', 286 | message: `Successfully created gist ID [${data.id}] and backed up the DB.`, 287 | options: { 288 | icon: 'mark-github' 289 | }, 290 | gistId: data.id 291 | }); 292 | return; 293 | } 294 | 295 | // failed to craete gist, response status code was not 201 Created 296 | reject({ 297 | type: 'addWarning', 298 | message: 'Failed to create gist.', 299 | options: { 300 | icon: 'mark-github' 301 | } 302 | }); 303 | }).catch(error => { 304 | reject(this.connectionError(error)); 305 | }); 306 | }); 307 | 308 | return promise; 309 | }, 310 | // orchestration function for update operation 311 | updateOperation: function updateOperation(value) { 312 | const promise = new Promise((resolve, reject) => { 313 | if (this.gistId) { 314 | // user provided gist id, check if gist exists (if yes update, otherwise reject) 315 | this.checkIfGistExists() 316 | .then(data => { 317 | this.updateGist(value, data.files).then(resolve).catch(reject); 318 | }).catch(reject); 319 | } else { 320 | // user didnt specify gist id, create gist for him and set it in config 321 | this.createNewGist(value).then(resolve).catch(reject); 322 | } 323 | }); 324 | 325 | return promise; 326 | }, 327 | // orchestration function for fetch operation 328 | fetchOperation: function fetchOperation() { 329 | const promise = new Promise((resolve, reject) => { 330 | this.getGist().then(resolve).catch(reject); 331 | }); 332 | 333 | return promise; 334 | }, 335 | // helper function to check whether the response status code equals the requiredStatusCode and return json promise if so 336 | // we then check in Promise.then() if data exists (exists only if response respones status code matched) 337 | toJson: function toJson(requiredStatusCode, response) { 338 | if (response.status == requiredStatusCode) { 339 | return response.json(); 340 | } 341 | return Promise.resolve(undefined); 342 | } 343 | }; 344 | 345 | onmessage = function(e) { 346 | if (!e.data || e.data.length === 0) { 347 | return; 348 | } 349 | 350 | if (e.data[0].hasOwnProperty('token')) { 351 | api.token = e.data[0].token; 352 | } 353 | 354 | if (e.data[0].hasOwnProperty('gistId')) { 355 | api.gistId = e.data[0].gistId; 356 | } 357 | 358 | if (e.data[0].hasOwnProperty('setName')) { 359 | api.setName = e.data[0].setName; 360 | } 361 | 362 | if (e.data[0].action === 'fetch') { 363 | Promise.all([api.checkConfig(api.token, 'Github Access Token'), api.checkConfig(api.gistId, 'Gist ID')]) 364 | .then(() => { 365 | api.fetchOperation() 366 | .then(postMessage) 367 | .catch(postMessage); 368 | }) 369 | .catch(postMessage); 370 | } else if (e.data[0].action === 'update') { 371 | Promise.all([api.checkConfig(api.token, 'Github Access Token')]) 372 | .then(() => { 373 | api.updateOperation(e.data[0].value) 374 | .then(postMessage) 375 | .catch(postMessage); 376 | }) 377 | .catch(postMessage); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /styles/project-viewer.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | @import "pv-variables"; 3 | @import (inline) "../node_modules/devicons/css/devicons.min.css"; 4 | 5 | li[is="tabs-tab"][data-type="project-viewer-editor"] { 6 | &:before { 7 | background-color: @project-viewer_item_color; 8 | } 9 | } 10 | 11 | project-viewer-editor { 12 | background-color: @tab-background-color; 13 | padding: @component-padding * 4 0; 14 | 15 | .panel-head { 16 | width: 80%; 17 | margin: 0 auto; 18 | } 19 | 20 | .panel-body { 21 | margin: 0 auto; 22 | height: 100%; 23 | overflow: scroll; 24 | padding: 0 10% 32px 10%; 25 | } 26 | 27 | .pv-editor-header::before { 28 | content: ''; 29 | border: 2px solid @project-viewer_item_color; 30 | margin-right: @component-padding; 31 | } 32 | 33 | .pv-editor-block { 34 | padding-bottom: @component-padding * 2; 35 | 36 | &.hidden { 37 | display: none; 38 | } 39 | 40 | .list-tree.groups-list { 41 | position: relative; 42 | } 43 | 44 | .list-item { 45 | padding-left: @component-padding * 2; 46 | } 47 | } 48 | 49 | input, 50 | .btn { 51 | margin-bottom: @component-padding / 2; 52 | margin-right: @component-padding / 2; 53 | } 54 | 55 | .input-color { 56 | border: none; 57 | cursor: pointer; 58 | transition: opacity @pv_transition-duration-default; 59 | 60 | &[disabled] { 61 | cursor: not-allowed; 62 | opacity: 0.1; 63 | } 64 | } 65 | 66 | .input-select { 67 | cursor: pointer; 68 | } 69 | 70 | .input-checkbox { 71 | cursor: pointer; 72 | } 73 | 74 | .pv-color-palette { 75 | display: inline-block; 76 | margin-left: @component-padding * 2; 77 | margin-bottom: @component-padding; 78 | vertical-align: middle; 79 | transition: opacity @pv_transition-duration-default; 80 | 81 | &[hidden] { 82 | opacity: 0.1; 83 | 84 | .pv-palette { 85 | cursor: not-allowed; 86 | } 87 | } 88 | } 89 | 90 | .pv-palette-info { 91 | margin-bottom: 2px; 92 | } 93 | 94 | .pv-path-remove { 95 | cursor: pointer; 96 | } 97 | 98 | .pv-palette { 99 | display: inline-block; 100 | height: 20px; 101 | width: 20px; 102 | border-radius: 5px; 103 | margin: 0 2px; 104 | cursor: pointer; 105 | } 106 | 107 | .input-checkbox.input-checkbox:focus, 108 | .input-radio.input-radio:focus { 109 | outline: 1px solid; 110 | } 111 | 112 | .input-label { 113 | cursor: pointer; 114 | display: block; 115 | width: -webkit-max-content; 116 | margin: 0 1em 1em 0; 117 | 118 | &.inliner { 119 | display: inline-block; 120 | } 121 | } 122 | 123 | .list-group { 124 | padding-top: @component-padding; 125 | } 126 | 127 | .icons-list { 128 | margin-top: @component-padding; 129 | height: 200px; 130 | overflow: scroll; 131 | padding: @component-padding / 2; 132 | background-color: @tool-panel-background-color; 133 | position: relative; 134 | flex: 1 1 300px; 135 | min-width: 0; 136 | border: 1px solid @tool-panel-border-color; 137 | display: flex; 138 | flex-wrap: wrap; 139 | align-content: flex-start; 140 | 141 | &.only-icons { 142 | .icon, .devicons { 143 | flex: 0 0 32px; 144 | height: 32px; 145 | margin: 3px; 146 | text-align: center; 147 | padding: 0; 148 | 149 | &:before{ 150 | margin: 0; 151 | font-size: 32px; 152 | width: 32px; 153 | height: 32px; 154 | vertical-align: middle; 155 | } 156 | } 157 | } 158 | 159 | .icon, .devicons { 160 | cursor: pointer; 161 | flex: 1 0 200px; 162 | padding: @component-padding/2 @component-padding; 163 | transition: all @pv_transition-duration-default; 164 | 165 | &:hover:not(.highlight-success) { 166 | color: @text-color-highlight; 167 | background-color: @button-background-color-hover; 168 | } 169 | 170 | &.hidden { 171 | display: none; 172 | } 173 | } 174 | } 175 | 176 | .list-tree { 177 | .list-nested-item { 178 | -webkit-user-select: none; 179 | pointer-events: none; 180 | 181 | .list-item { 182 | pointer-events: all; 183 | cursor: pointer; 184 | transition: color @pv_transition-duration-default ease-in 0s; 185 | 186 | &:hover { 187 | color: @project-viewer_item_color; 188 | } 189 | } 190 | 191 | .list-tree { 192 | pointer-events: none; 193 | } 194 | } 195 | } 196 | } 197 | 198 | .pv-has-icons { 199 | 200 | .devicons { 201 | font-family: @font-family; 202 | font-size: @font-size; 203 | display: inline-block; 204 | text-align: left; 205 | top: 0; 206 | 207 | &:before { 208 | font-family: 'devicons'; 209 | font-size: 16px; 210 | margin-right: 5px; 211 | vertical-align: middle; 212 | height: inherit; 213 | } 214 | } 215 | 216 | .list-group .primary-line.devicons { 217 | display: block; 218 | text-align: left; 219 | } 220 | } 221 | 222 | project-viewer { 223 | 224 | &.position-absolute { 225 | position: absolute; 226 | top: 0; 227 | bottom: 0; 228 | z-index: 100; 229 | 230 | &.position-right { 231 | right: 0; 232 | } 233 | 234 | &.position-left { 235 | left: 0; 236 | } 237 | } 238 | 239 | .devicons:before { 240 | font-family: 'devicons'; 241 | font-size: @component-icon-size; 242 | margin-right: @component-padding / 2; 243 | vertical-align: middle; 244 | height: inherit; 245 | } 246 | 247 | -webkit-user-select: none; 248 | font-size: @font-size; 249 | font-family: @font-family; 250 | display: flex; 251 | flex-direction: column; 252 | // transition: width 0.5s; 253 | width: @project-viewer_main_width-max; 254 | overflow: hidden; 255 | 256 | &.resizing { 257 | transition: none; 258 | } 259 | 260 | .heading { 261 | color: @project-viewer_item_color; 262 | padding-left: @component-padding; 263 | white-space: nowrap; 264 | 265 | &.hidden { 266 | visibility: hidden; 267 | } 268 | } 269 | 270 | &.autohide { 271 | width: @project-viewer_main_width-min; 272 | 273 | .hidden-block { 274 | opacity: 0; 275 | } 276 | 277 | &:hover { 278 | width: @project-viewer_main_width-max; 279 | 280 | .hidden-block { 281 | opacity: 1; 282 | } 283 | } 284 | } 285 | 286 | .hidden-block { 287 | background-color: @tool-panel-background-color; 288 | position: absolute; 289 | height: 100%; 290 | width: 100%; 291 | z-index: 1; 292 | transition: opacity 0.5s; 293 | overflow: hidden; 294 | } 295 | 296 | .body-content { 297 | overflow: scroll; 298 | height: calc(~"100% - 50px"); 299 | width: 100%; 300 | flex-grow: 1; 301 | position: absolute; 302 | } 303 | 304 | > .list-tree { 305 | position: relative; 306 | padding: @component-padding @component-padding/2; 307 | } 308 | 309 | .background-message { 310 | transition: opacity @pv_transition-duration-default; 311 | 312 | &.hidden { 313 | opacity: 0; 314 | } 315 | } 316 | 317 | .has-collapsable-children { 318 | margin-left: 10px; 319 | } 320 | 321 | li[is="project-viewer-group"] { 322 | 323 | &.above { 324 | > .list-item { 325 | color: @project-viewer_item_color; 326 | 327 | &::after { 328 | position: absolute; 329 | content: ""; 330 | left: 0; 331 | height: @component-line-height / 2; 332 | width: @component-line-height / 3; 333 | border-color: @project-viewer_item_color; 334 | border-style: solid; 335 | border-width: thin; 336 | border-bottom: transparent; 337 | border-right: transparent; 338 | } 339 | } 340 | } 341 | 342 | &.below { 343 | 344 | > .list-item { 345 | color: @project-viewer_item_color; 346 | 347 | &::after { 348 | position: absolute; 349 | content: ""; 350 | left: 0; 351 | height: @component-line-height / 2; 352 | width: @component-line-height / 3; 353 | margin-top: @component-line-height / 2; 354 | border-color: @project-viewer_item_color; 355 | border-style: solid; 356 | border-width: thin; 357 | border-top: transparent; 358 | border-right: transparent; 359 | } 360 | } 361 | } 362 | 363 | &.center { 364 | > .list-item { 365 | color: @project-viewer_item_color; 366 | 367 | &::after { 368 | position: absolute; 369 | content: ""; 370 | left: 0; 371 | height: @component-line-height / 2; 372 | margin-top: @component-line-height / 3; 373 | border-color: @project-viewer_item_color; 374 | border-style: solid; 375 | border-width: thin; 376 | border-top: transparent; 377 | border-right: transparent; 378 | border-bottom: transparent; 379 | } 380 | } 381 | } 382 | } 383 | 384 | li[is="project-viewer-group"].list-nested-item, 385 | li[is="project-viewer-project"].list-item { 386 | transition: opacity @pv_transition-duration-default; 387 | 388 | &.dragged { 389 | opacity: 0.5; 390 | 391 | > .list-item { 392 | color: @project-viewer_item_color_dragged !important; 393 | } 394 | 395 | .list-tree { 396 | display: none; 397 | } 398 | } 399 | 400 | &:not(.selected)::before { 401 | position: absolute; 402 | left: 0; 403 | height: @component-line-height; 404 | content: ""; 405 | width: 2px; 406 | background-color: @project-viewer_item_background-color; 407 | visibility: hidden; 408 | transform: scaleY(0); 409 | color: @project-viewer_item_color; 410 | transition: transform @pv_transition-duration-default, visibility @pv_transition-duration-default; 411 | padding-left: 0; 412 | } 413 | 414 | &.active { 415 | color: @project-viewer_item_color; 416 | 417 | > .list-item { 418 | color: @project-viewer_item_color; 419 | } 420 | 421 | &:not(.selected):not(.no-paths)::before { 422 | visibility: visible !important; 423 | transform: scaleY(1) !important; 424 | } 425 | } 426 | } 427 | 428 | li[is="project-viewer-group"].list-nested-item { 429 | -webkit-user-drag: element; 430 | pointer-events: none; 431 | transition: color @pv_transition-duration-default ease-in 0s; 432 | 433 | > .list-item { 434 | pointer-events: all; 435 | cursor: pointer; 436 | 437 | &:hover { 438 | color: @project-viewer_item_color; 439 | } 440 | } 441 | 442 | .list-tree { 443 | pointer-events: none; 444 | } 445 | } 446 | 447 | li[is="project-viewer-project"].list-item { 448 | -webkit-user-drag: element; 449 | -webkit-user-select: none; 450 | cursor: pointer; 451 | transition: color @pv_transition-duration-default ease-in 0s, background-color @pv_transition-duration-default ease-in 0s, padding @pv_transition-duration-fast ease-in 0s; 452 | pointer-events: all; 453 | 454 | &.no-paths, 455 | &.no-paths span { 456 | text-decoration: line-through; 457 | color: @text-color-subtle; 458 | cursor: not-allowed; 459 | } 460 | 461 | &:not(.selected):not(.no-paths)::before { 462 | position: absolute; 463 | left: 0; 464 | height: @component-line-height; 465 | content: ""; 466 | width: 2px; 467 | background-color: @project-viewer_item_background-color; 468 | visibility: hidden; 469 | transform: scaleY(0); 470 | transition: transform @pv_transition-duration-default, visibility @pv_transition-duration-default; 471 | } 472 | 473 | &:not(.selected):not(.no-paths):hover { 474 | color: @project-viewer_item_color; 475 | padding-left: 0; 476 | } 477 | 478 | &:not(.dragging):not(.selected):hover::before { 479 | visibility: visible; 480 | transform: scaleY(1); 481 | } 482 | 483 | span { 484 | pointer-events: none; 485 | } 486 | 487 | &.dropping { 488 | padding-top: 0; 489 | padding-bottom: 0; 490 | color: @project-viewer_item_color; 491 | 492 | &.above { 493 | padding-top: 5px; 494 | padding-bottom: 0; 495 | } 496 | 497 | &.below { 498 | padding-top: 0; 499 | padding-bottom: 5px; 500 | } 501 | } 502 | } 503 | } 504 | 505 | .pv-resizer { 506 | position: absolute; 507 | cursor: ew-resize; 508 | top: 0; 509 | width: 4px; 510 | height: 100%; 511 | display: block; 512 | opacity: 1; 513 | transition: background-color @pv_transition-duration-default; 514 | 515 | &.invert { 516 | right: 0; 517 | } 518 | 519 | &:hover { 520 | opacity: 0.5; 521 | background-color: @background-color-highlight; 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /styles/pv-variables.less: -------------------------------------------------------------------------------- 1 | @pv_transition-duration-default: 0.3s; 2 | @pv_transition-duration-fast: 0.1s; 3 | 4 | @project-viewer_main_width-max: 200px; 5 | //@project-viewer_main_width-max: 100%; 6 | @project-viewer_main_width-min: 20px; 7 | @project-viewer_item_background-color: @ui-site-color-2; 8 | @project-viewer_item_color: @ui-site-color-2; 9 | @project-viewer_item_color_dragged: @ui-site-color-1; 10 | --------------------------------------------------------------------------------