mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-11 11:15:24 +03:00
Incorporate settings-view to core
Co-Authored-By: @Sertonix <83883937+Sertonix@users.noreply.github.com>
This commit is contained in:
parent
3646a714cf
commit
34f4c70d63
@ -153,7 +153,7 @@
|
||||
"season": "^6.0.2",
|
||||
"semver": "7.3.2",
|
||||
"service-hub": "^0.7.4",
|
||||
"settings-view": "https://github.com/pulsar-edit/settings-view.git#v0.261.11",
|
||||
"settings-view": "file:packages/settings-view",
|
||||
"sinon": "9.2.1",
|
||||
"snippets": "https://codeload.github.com/pulsar-edit/snippets/legacy.tar.gz/refs/tags/v1.6.1",
|
||||
"solarized-dark-syntax": "file:packages/solarized-dark-syntax",
|
||||
|
@ -84,7 +84,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
|
||||
| **one-light-ui** | [`./one-light-ui`](./one-light-ui) | |
|
||||
| **open-on-github** | [`atom/open-on-github`][open-on-github] | [#18278](https://github.com/atom/atom/issues/18278) |
|
||||
| **package-generator** | [`atom/package-generator`][package-generator] | [#18279](https://github.com/atom/atom/issues/18279) |
|
||||
| **settings-view** | [`atom/settings-view`][settings-view] | |
|
||||
| **settings-view** | [`./settings-view`](./settings-view) | |
|
||||
| **snippets** | [`atom/snippets`][snippets] | |
|
||||
| **solarized-dark-syntax** | [`./solarized-dark-syntax`](./solarized-dark-syntax) | |
|
||||
| **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | |
|
||||
@ -121,7 +121,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
|
||||
[notifications]: https://github.com/pulsar-edit/notifications
|
||||
[open-on-github]: https://github.com/pulsar-edit/open-on-github
|
||||
[package-generator]: https://github.com/pulsar-edit/package-generator
|
||||
[settings-view]: https://github.com/pulsar-edit/settings-view
|
||||
[snippets]: https://github.com/pulsar-edit/snippets
|
||||
[spell-check]: https://github.com/pulsar-edit/spell-check
|
||||
[status-bar]: https://github.com/pulsar-edit/status-bar
|
||||
|
15
packages/settings-view/.github/no-response.yml
vendored
Normal file
15
packages/settings-view/.github/no-response.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||
|
||||
# Number of days of inactivity before an issue is closed for lack of response
|
||||
daysUntilClose: 28
|
||||
|
||||
# Label requiring a response
|
||||
responseRequiredLabel: more-information-needed
|
||||
|
||||
# Comment to post when closing an issue for lack of response. Set to `false` to disable.
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there has been no response
|
||||
to our request for more information from the original author. With only the
|
||||
information that is currently in the issue, we don't have enough information
|
||||
to take action. Please reach out if you have or find the answers we need so
|
||||
that we can investigate further.
|
25
packages/settings-view/.github/workflows/pulsar-ci.yml
vendored
Normal file
25
packages/settings-view/.github/workflows/pulsar-ci.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Tests
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-latest, windows-2019]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout the Latest Package Code
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pulsar Editor
|
||||
uses: pulsar-edit/action-pulsar-dependency@v2
|
||||
with:
|
||||
package-to-test: "settings-view"
|
||||
- name: Run the Headless Pulsar Tests
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
with:
|
||||
run: yarn start --test spec
|
||||
working-directory: ./pulsar
|
2
packages/settings-view/.gitignore
vendored
Normal file
2
packages/settings-view/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
npm-debug.log
|
1
packages/settings-view/CONTRIBUTING.md
Normal file
1
packages/settings-view/CONTRIBUTING.md
Normal file
@ -0,0 +1 @@
|
||||
[See how you can contribute](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md)
|
57
packages/settings-view/README.md
Normal file
57
packages/settings-view/README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Settings View package
|
||||
|
||||
Edit core configuration settings, install and configure packages, and change themes from within Pulsar.
|
||||
|
||||
![Settings View](https://cloud.githubusercontent.com/assets/118951/16886698/b0ca5fae-4a8a-11e6-8afc-2c03fda4618c.PNG)
|
||||
|
||||
## Usage
|
||||
You can open the Settings View by navigating to
|
||||
***LNX***: _Edit > Preferences_ -
|
||||
***MAC***: _Atom > Preferences_ -
|
||||
***WIN***: _File > Settings_.
|
||||
|
||||
In order to install new packages and themes, click on the _Install_ section on the left-hand side.
|
||||
Once installed, community packages/themes and their settings are housed within their respective section.
|
||||
All packages/themes that have updates will be listed under the _Updates_ section. Finally, all keybindings (including ones that community packages have added) are available in the _Keybindings_ section.
|
||||
|
||||
Want to learn more? Check out the [Getting Started: Pulsar Basics](https://pulsar-edit.dev/docs/launch-manual/sections/getting-started/#pulsar-basics) and [Using Pulsar: Pulsar Packages](https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#pulsar-packages) sections in the Pulsar Launch Manual.
|
||||
|
||||
### Commands and Keybindings
|
||||
All of the following commands are under the `atom-workspace` selector.
|
||||
|
||||
|Command|Description|Keybinding (Linux)|Keybinding (macOS)|Keybinding (Windows)|
|
||||
|-------|-----------|------------------|-----------------|--------------------|
|
||||
|`settings-view:open`|Opens the Settings View|<kbd>ctrl-,</kbd>|<kbd>cmd-,</kbd>|<kbd>ctrl-,</kbd>|
|
||||
|`settings-view:core`|Opens the _Core_ section of the Settings View|
|
||||
|`settings-view:editor`|Opens the _Editor_ section of the Settings View|
|
||||
|`settings-view:system`|Opens the _System_ section of the Settings View (Windows)|
|
||||
|`settings-view:show-keybindings`|Opens the _Keybindings_ section of the Settings View|
|
||||
|`settings-view:uninstall-packages`|Opens the _Packages_ section of the Settings View|
|
||||
|`settings-view:change-themes`|Opens the _Themes_ section of the Settings View|
|
||||
|`settings-view:uninstall-themes`|Opens the _Themes_ section of the Settings View|
|
||||
|`settings-view:check-for-updates`|Opens the _Updates_ section of the Settings View|
|
||||
|`settings-view:install-packages-and-themes`|Opens the _Install_ section of the Settings View|
|
||||
Custom keybindings can be added by referencing the above commands. To learn more, visit the [Using Pulsar: Basic Customization](https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#basic-customization) or [Behind Pulsar: Keymaps In-Depth](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar/#keymaps-in-depth) sections in the Pulsar Launch Manual.
|
||||
|
||||
## Customize
|
||||
The Settings View package uses the `ui-variables` to match a theme's color scheme. You can still customize the UI in your `styles.less` file. For example:
|
||||
|
||||
```less
|
||||
// Change the color of the titles
|
||||
.settings-view .section .section-heading {
|
||||
color: white;
|
||||
}
|
||||
|
||||
// Change the font size of the setting descriptions
|
||||
.settings-view .setting-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
```
|
||||
|
||||
Use the [developer tools](https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#developer-tools) to find more selectors.
|
||||
|
||||
## Contributing
|
||||
Always feel free to help out! Whether it's [filing bugs and feature requests](https://github.com/pulsar-edit/pulsar/issues/new) or working on some of the [open issues](https://github.com/pulsar-edit/pulsar/issues), Pulsar's [contributing guide](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md) will help get you started while the [guide for contributing to packages](https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#contributing-to-official-pulsar-packages) has some extra information.
|
||||
|
||||
## License
|
||||
MIT License. See [the license](../../LICENSE.md) for more details.
|
5
packages/settings-view/keymaps/settings-view.cson
Normal file
5
packages/settings-view/keymaps/settings-view.cson
Normal file
@ -0,0 +1,5 @@
|
||||
'.platform-darwin':
|
||||
'cmd-,': 'settings-view:open'
|
||||
|
||||
'.platform-win32, .platform-linux':
|
||||
'ctrl-,': 'settings-view:open'
|
226
packages/settings-view/lib/atom-io-client.coffee
Normal file
226
packages/settings-view/lib/atom-io-client.coffee
Normal file
@ -0,0 +1,226 @@
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{remote} = require 'electron'
|
||||
|
||||
glob = require 'glob'
|
||||
request = require 'request'
|
||||
|
||||
module.exports =
|
||||
class AtomIoClient
|
||||
constructor: (@packageManager, @baseURL) ->
|
||||
@baseURL ?= 'https://api.pulsar-edit.dev/api/'
|
||||
# 12 hour expiry
|
||||
@expiry = 1000 * 60 * 60 * 12
|
||||
@createAvatarCache()
|
||||
@expireAvatarCache()
|
||||
|
||||
# Public: Get an avatar image from the filesystem, fetching it first if necessary
|
||||
avatar: (login, callback) ->
|
||||
@cachedAvatar login, (err, cached) =>
|
||||
stale = Date.now() - parseInt(cached.split('-').pop()) > @expiry if cached
|
||||
if cached and (not stale or not @online())
|
||||
callback null, cached
|
||||
else
|
||||
@fetchAndCacheAvatar(login, callback)
|
||||
|
||||
# Public: get a package from the atom.io API, with the appropriate level of
|
||||
# caching.
|
||||
package: (name, callback) ->
|
||||
packagePath = "packages/#{name}"
|
||||
data = @fetchFromCache(packagePath)
|
||||
if data
|
||||
callback(null, data)
|
||||
else
|
||||
@request(packagePath, callback)
|
||||
|
||||
featuredPackages: (callback) ->
|
||||
# TODO clean up caching copypasta
|
||||
data = @fetchFromCache 'packages/featured'
|
||||
if data
|
||||
callback(null, data)
|
||||
else
|
||||
@getFeatured(false, callback)
|
||||
|
||||
featuredThemes: (callback) ->
|
||||
# TODO clean up caching copypasta
|
||||
data = @fetchFromCache 'themes/featured'
|
||||
if data
|
||||
callback(null, data)
|
||||
else
|
||||
@getFeatured(true, callback)
|
||||
|
||||
getFeatured: (loadThemes, callback) ->
|
||||
# apm already does this, might as well use it instead of request i guess? The
|
||||
# downside is that I need to repeat caching logic here.
|
||||
@packageManager.getFeatured(loadThemes)
|
||||
.then (packages) =>
|
||||
# copypasta from below
|
||||
key = if loadThemes then 'themes/featured' else 'packages/featured'
|
||||
cached =
|
||||
data: packages
|
||||
createdOn: Date.now()
|
||||
localStorage.setItem(@cacheKeyForPath(key), JSON.stringify(cached))
|
||||
# end copypasta
|
||||
callback(null, packages)
|
||||
.catch (error) ->
|
||||
callback(error, null)
|
||||
|
||||
request: (path, callback) ->
|
||||
options = {
|
||||
url: "#{@baseURL}#{path}"
|
||||
headers: {'User-Agent': navigator.userAgent}
|
||||
gzip: true
|
||||
}
|
||||
|
||||
request options, (err, res, body) =>
|
||||
return callback(err) if err
|
||||
|
||||
try
|
||||
# NOTE: request's json option does not populate err if parsing fails,
|
||||
# so we do it manually
|
||||
body = @parseJSON(body)
|
||||
delete body.versions
|
||||
|
||||
cached =
|
||||
data: body
|
||||
createdOn: Date.now()
|
||||
localStorage.setItem(@cacheKeyForPath(path), JSON.stringify(cached))
|
||||
callback(err, cached.data)
|
||||
catch error
|
||||
callback(error)
|
||||
|
||||
cacheKeyForPath: (path) ->
|
||||
"settings-view:#{path}"
|
||||
|
||||
online: ->
|
||||
navigator.onLine
|
||||
|
||||
# This could use a better name, since it checks whether it's appropriate to return
|
||||
# the cached data and pretends it's null if it's stale and we're online
|
||||
fetchFromCache: (packagePath) ->
|
||||
cached = localStorage.getItem(@cacheKeyForPath(packagePath))
|
||||
cached = if cached then @parseJSON(cached)
|
||||
if cached? and (not @online() or Date.now() - cached.createdOn < @expiry)
|
||||
return cached.data
|
||||
else
|
||||
# falsy data means "try to hit the network"
|
||||
return null
|
||||
|
||||
createAvatarCache: ->
|
||||
fs.makeTree(@getCachePath())
|
||||
|
||||
avatarPath: (login) ->
|
||||
path.join @getCachePath(), "#{login}-#{Date.now()}"
|
||||
|
||||
cachedAvatar: (login, callback) ->
|
||||
glob @avatarGlob(login), (err, files) =>
|
||||
return callback(err) if err
|
||||
files.sort().reverse()
|
||||
for imagePath in files
|
||||
filename = path.basename(imagePath)
|
||||
[..., createdOn] = filename.split('-')
|
||||
if Date.now() - parseInt(createdOn) < @expiry
|
||||
return callback(null, imagePath)
|
||||
callback(null, null)
|
||||
|
||||
avatarGlob: (login) ->
|
||||
path.join @getCachePath(), "#{login}-*([0-9])"
|
||||
|
||||
fetchAndCacheAvatar: (login, callback) ->
|
||||
if not @online()
|
||||
callback(null, null)
|
||||
else
|
||||
imagePath = @avatarPath login
|
||||
requestObject = {
|
||||
url: "https://avatars.githubusercontent.com/#{login}"
|
||||
headers: {'User-Agent': navigator.userAgent}
|
||||
}
|
||||
request.head requestObject, (error, response, body) ->
|
||||
if error? or response.statusCode isnt 200 or not response.headers['content-type'].startsWith('image/')
|
||||
callback(error)
|
||||
else
|
||||
writeStream = fs.createWriteStream imagePath
|
||||
writeStream.on 'finish', -> callback(null, imagePath)
|
||||
writeStream.on 'error', (error) ->
|
||||
writeStream.close()
|
||||
try
|
||||
fs.unlinkSync imagePath if fs.existsSync imagePath
|
||||
callback(error)
|
||||
request(requestObject).pipe(writeStream)
|
||||
|
||||
# The cache expiry doesn't need to be clever, or even compare dates, it just
|
||||
# needs to always keep around the newest item, and that item only. The localStorage
|
||||
# cache updates in place, so it doesn't need to be purged.
|
||||
|
||||
expireAvatarCache: ->
|
||||
deleteAvatar = (child) =>
|
||||
avatarPath = path.join(@getCachePath(), child)
|
||||
fs.unlink avatarPath, (error) ->
|
||||
if error and error.code isnt 'ENOENT' # Ignore cache paths that don't exist
|
||||
console.warn("Error deleting avatar (#{error.code}): #{avatarPath}")
|
||||
|
||||
fs.readdir @getCachePath(), (error, _files) ->
|
||||
_files ?= []
|
||||
files = {}
|
||||
for filename in _files
|
||||
parts = filename.split('-')
|
||||
stamp = parts.pop()
|
||||
key = parts.join('-')
|
||||
files[key] ?= []
|
||||
files[key].push "#{key}-#{stamp}"
|
||||
|
||||
for key, children of files
|
||||
children.sort()
|
||||
children.pop() # keep
|
||||
# Right now a bunch of clients might be instantiated at once, so
|
||||
# we can just ignore attempts to unlink files that have already been removed
|
||||
# - this should be fixed with a singleton client
|
||||
children.forEach(deleteAvatar)
|
||||
|
||||
getCachePath: ->
|
||||
@cachePath ?= path.join(remote.app.getPath('userData'), 'Cache', 'settings-view')
|
||||
|
||||
search: (query, options) ->
|
||||
qs = {q: query}
|
||||
|
||||
if options.themes
|
||||
qs.filter = 'theme'
|
||||
else if options.packages
|
||||
qs.filter = 'package'
|
||||
|
||||
options = {
|
||||
url: "#{@baseURL}packages/search"
|
||||
headers: {'User-Agent': navigator.userAgent}
|
||||
qs: qs
|
||||
gzip: true
|
||||
}
|
||||
|
||||
new Promise (resolve, reject) =>
|
||||
request options, (err, res, textBody) =>
|
||||
if err
|
||||
error = new Error("Searching for \u201C#{query}\u201D failed.")
|
||||
error.stderr = err.message
|
||||
reject(error)
|
||||
else
|
||||
try
|
||||
# NOTE: request's json option does not populate err if parsing fails,
|
||||
# so we do it manually
|
||||
body = @parseJSON(textBody)
|
||||
if body.filter
|
||||
resolve(
|
||||
body.filter (pkg) -> pkg.releases?.latest?
|
||||
.map ({readme, metadata, downloads, stargazers_count, repository}) ->
|
||||
Object.assign metadata, {readme, downloads, stargazers_count, repository: repository.url}
|
||||
)
|
||||
else
|
||||
error = new Error("Searching for \u201C#{query}\u201D failed.\n")
|
||||
error.stderr = "API returned: " + textBody
|
||||
reject error
|
||||
|
||||
catch e
|
||||
error = new Error("Searching for \u201C#{query}\u201D failed.")
|
||||
error.stderr = e.message + '\n' + textBody
|
||||
reject error
|
||||
|
||||
parseJSON: (s) ->
|
||||
JSON.parse(s)
|
53
packages/settings-view/lib/collapsible-section-panel.js
Normal file
53
packages/settings-view/lib/collapsible-section-panel.js
Normal file
@ -0,0 +1,53 @@
|
||||
/** @babel */
|
||||
|
||||
import {Disposable} from 'atom'
|
||||
|
||||
export default class CollapsibleSectionPanel {
|
||||
notHiddenCardsLength (sectionElement) {
|
||||
return sectionElement.querySelectorAll('.package-card:not(.hidden)').length
|
||||
}
|
||||
|
||||
updateSectionCount (headerElement, countElement, packageCount, totalCount) {
|
||||
if (totalCount != null) {
|
||||
countElement.textContent = `${packageCount}/${totalCount}`
|
||||
} else {
|
||||
countElement.textContent = packageCount
|
||||
}
|
||||
|
||||
if (packageCount > 0) {
|
||||
headerElement.classList.add('has-items')
|
||||
}
|
||||
}
|
||||
|
||||
updateSectionCounts () {
|
||||
this.resetSectionHasItems()
|
||||
|
||||
const filterText = this.refs.filterEditor.getText()
|
||||
if (filterText === '') {
|
||||
this.updateUnfilteredSectionCounts()
|
||||
} else {
|
||||
this.updateFilteredSectionCounts()
|
||||
}
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
const handler = (e) => {
|
||||
const target = e.target.closest('.sub-section .has-items')
|
||||
if (target) {
|
||||
target.parentNode.classList.toggle('collapsed')
|
||||
}
|
||||
}
|
||||
this.element.addEventListener('click', handler)
|
||||
return new Disposable(() => this.element.removeEventListener('click', handler))
|
||||
}
|
||||
|
||||
resetCollapsibleSections (headerSections) {
|
||||
for (const headerSection of headerSections) {
|
||||
this.resetCollapsibleSection(headerSection)
|
||||
}
|
||||
}
|
||||
|
||||
resetCollapsibleSection (headerSection) {
|
||||
headerSection.classList.remove('has-items')
|
||||
}
|
||||
}
|
78
packages/settings-view/lib/editor-panel.js
Normal file
78
packages/settings-view/lib/editor-panel.js
Normal file
@ -0,0 +1,78 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
import SettingsPanel from './settings-panel'
|
||||
|
||||
export default class EditorPanel {
|
||||
constructor () {
|
||||
etch.initialize(this)
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div tabIndex='0' className='panels-item' onclick={this.didClick}>
|
||||
<SettingsPanel
|
||||
namespace='editor'
|
||||
icon='code'
|
||||
note={`<div class="text icon icon-question" id="editor-settings-note" tabindex="-1">These settings are related to text editing. Some of these can be overriden on a per-language basis. Check language settings by clicking its package card in the <a class="link packages-open">Packages list</a>.</div>`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
didClick (event) {
|
||||
const target = event.target.closest('.packages-open')
|
||||
if (target) {
|
||||
atom.workspace.open('atom://config/packages')
|
||||
}
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
82
packages/settings-view/lib/error-view.js
Normal file
82
packages/settings-view/lib/error-view.js
Normal file
@ -0,0 +1,82 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import etch from 'etch'
|
||||
|
||||
export default class ErrorView {
|
||||
constructor (packageManager, {message, stderr, packageInstallError}) {
|
||||
etch.initialize(this)
|
||||
|
||||
this.isOutputHidden = true
|
||||
this.refs.detailsArea.style.display = 'none'
|
||||
this.refs.details.textContent = stderr
|
||||
this.refs.message.textContent = message
|
||||
|
||||
// Check for native build tools and show warning if missing.
|
||||
if (packageInstallError && process.platform === 'win32') {
|
||||
packageManager.checkNativeBuildTools().catch(() => {
|
||||
this.refs.alert.appendChild(new CompileToolsErrorView().element)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
destroy () {
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='error-message'>
|
||||
<div ref='alert' className='alert alert-danger alert-dismissable native-key-bindings' tabIndex='-1'>
|
||||
<button ref='close' className='close icon icon-x' onclick={() => this.destroy()} />
|
||||
<span ref='message' className='native-key-bindings' />
|
||||
<a ref='detailsLink' className='alert-link error-link' onclick={() => this.toggleOutput()}>{'Show output\u2026'}</a>
|
||||
<div ref='detailsArea' className='padded'>
|
||||
<pre ref='details' className='error-details text' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
toggleOutput () {
|
||||
if (this.isOutputHidden) {
|
||||
this.isOutputHidden = false
|
||||
this.refs.detailsArea.style.display = ''
|
||||
this.refs.detailsLink.textContent = 'Hide output\u2026'
|
||||
} else {
|
||||
this.isOutputHidden = true
|
||||
this.refs.detailsArea.style.display = 'none'
|
||||
this.refs.detailsLink.textContent = 'Show output\u2026'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CompileToolsErrorView {
|
||||
constructor () {
|
||||
etch.initialize(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<div className='icon icon-alert compile-tools-heading compile-tools-message'>Compiler tools not found</div>
|
||||
<div className='compile-tools-message'>Packages that depend on modules that contain C/C++ code will fail to install.</div>
|
||||
<div className='compile-tools-message'>
|
||||
<span>Read </span>
|
||||
<a className='link' href='https://github.com/atom/atom/blob/master/docs/build-instructions/windows.md'>here</a>
|
||||
<span> for instructions on installing Python and Visual Studio.</span>
|
||||
</div>
|
||||
<div className='compile-tools-message'>
|
||||
<span>Run </span>
|
||||
<code className='alert-danger'>apm install --check</code>
|
||||
<span> after installing to test compiling a native module.</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
79
packages/settings-view/lib/general-panel.js
Normal file
79
packages/settings-view/lib/general-panel.js
Normal file
@ -0,0 +1,79 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
import SettingsPanel from './settings-panel'
|
||||
|
||||
export default class GeneralPanel {
|
||||
constructor () {
|
||||
etch.initialize(this)
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div tabIndex='0' className='panels-item' onclick={this.didClick}>
|
||||
<SettingsPanel
|
||||
ref='panel'
|
||||
namespace='core'
|
||||
icon='settings'
|
||||
note={`<div class="text icon icon-question" id="core-settings-note" tabindex="-1">These are Atom's core settings which affect behavior unrelated to text editing. Individual packages may have their own additional settings found within their package card in the <a class="link packages-open">Packages list</a>.</div>`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
didClick (event) {
|
||||
const target = event.target.closest('.packages-open')
|
||||
if (target) {
|
||||
atom.workspace.open('atom://config/packages')
|
||||
}
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
342
packages/settings-view/lib/install-panel.js
Normal file
342
packages/settings-view/lib/install-panel.js
Normal file
@ -0,0 +1,342 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import path from 'path'
|
||||
import electron from 'electron'
|
||||
import etch from 'etch'
|
||||
import hostedGitInfo from 'hosted-git-info'
|
||||
|
||||
import {CompositeDisposable, TextEditor} from 'atom'
|
||||
|
||||
import PackageCard from './package-card'
|
||||
import ErrorView from './error-view'
|
||||
|
||||
const PackageNameRegex = /config\/install\/(package|theme):([a-z0-9-_]+)/i
|
||||
|
||||
export default class InstallPanel {
|
||||
constructor (settingsView, packageManager) {
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.client = this.packageManager.getClient()
|
||||
this.atomIoURL = 'https://pulsar-edit.dev/packages'
|
||||
|
||||
etch.initialize(this)
|
||||
|
||||
this.refs.searchMessage.style.display = 'none'
|
||||
|
||||
this.refs.searchEditor.setPlaceholderText('Search packages')
|
||||
this.searchType = 'packages'
|
||||
this.disposables.add(
|
||||
this.packageManager.on('package-install-failed', ({pack, error}) => {
|
||||
this.refs.searchErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
})
|
||||
)
|
||||
this.disposables.add(
|
||||
this.packageManager.on('package-installed theme-installed', ({pack}) => {
|
||||
const gitUrlInfo =
|
||||
(this.currentGitPackageCard && this.currentGitPackageCard.pack && this.currentGitPackageCard.pack.gitUrlInfo)
|
||||
? this.currentGitPackageCard.pack.gitUrlInfo
|
||||
: null
|
||||
|
||||
if (gitUrlInfo && gitUrlInfo === pack.gitUrlInfo) {
|
||||
this.updateGitPackageCard(pack)
|
||||
}
|
||||
})
|
||||
)
|
||||
this.disposables.add(
|
||||
this.refs.searchEditor.onDidStopChanging(() => {
|
||||
this.performSearch()
|
||||
})
|
||||
)
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
this.loadFeaturedPackages()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
focus () {
|
||||
this.refs.searchEditor.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='panels-item' tabIndex='-1'>
|
||||
<div className='section packages'>
|
||||
<div className='section-container'>
|
||||
<h1 ref='installHeading' className='section-heading icon icon-plus'>Install Packages</h1>
|
||||
|
||||
<div className='text native-key-bindings' tabIndex='-1'>
|
||||
<span className='icon icon-question' />
|
||||
<span ref='publishedToText'>Packages are published to </span>
|
||||
<a className='link' onclick={this.didClickOpenAtomIo.bind(this)}>atom.io</a>
|
||||
<span> and are installed to {path.join(process.env.ATOM_HOME, 'packages')}</span>
|
||||
</div>
|
||||
|
||||
<div className='search-container clearfix'>
|
||||
<div className='editor-container'>
|
||||
<TextEditor mini ref='searchEditor' />
|
||||
</div>
|
||||
<div className='btn-group'>
|
||||
<button ref='searchPackagesButton' className='btn btn-default selected' onclick={this.didClickSearchPackagesButton.bind(this)}>Packages</button>
|
||||
<button ref='searchThemesButton' className='btn btn-default' onclick={this.didClickSearchThemesButton.bind(this)}>Themes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref='searchErrors' />
|
||||
<div ref='searchMessage' className='alert alert-info search-message icon icon-search' />
|
||||
<div ref='resultsContainer' className='container package-container' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='section packages'>
|
||||
<div className='section-container'>
|
||||
<div ref='featuredHeading' className='section-heading icon icon-star' />
|
||||
<div ref='featuredErrors' />
|
||||
<div ref='loadingMessage' className='alert alert-info icon icon-hourglass' />
|
||||
<div ref='featuredContainer' className='container package-container' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
setSearchType (searchType) {
|
||||
if (searchType === 'theme') {
|
||||
this.searchType = 'themes'
|
||||
this.refs.searchThemesButton.classList.add('selected')
|
||||
this.refs.searchPackagesButton.classList.remove('selected')
|
||||
this.refs.searchEditor.setPlaceholderText('Search themes')
|
||||
this.refs.publishedToText.textContent = 'Themes are published to '
|
||||
this.atomIoURL = 'https://pulsar-edit.dev/themes'
|
||||
this.loadFeaturedPackages(true)
|
||||
} else if (searchType === 'package') {
|
||||
this.searchType = 'packages'
|
||||
this.refs.searchPackagesButton.classList.add('selected')
|
||||
this.refs.searchThemesButton.classList.remove('selected')
|
||||
this.refs.searchEditor.setPlaceholderText('Search packages')
|
||||
this.refs.publishedToText.textContent = 'Packages are published to '
|
||||
this.atomIoURL = 'https://pulsar-edit.dev/packages'
|
||||
this.loadFeaturedPackages()
|
||||
}
|
||||
}
|
||||
|
||||
beforeShow (options) {
|
||||
if (options && options.uri) {
|
||||
const query = this.extractQueryFromURI(options.uri)
|
||||
if (query != null) {
|
||||
const {searchType, packageName} = query
|
||||
this.setSearchType(searchType)
|
||||
this.refs.searchEditor.setText(packageName)
|
||||
this.performSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractQueryFromURI (uri) {
|
||||
const matches = PackageNameRegex.exec(uri)
|
||||
if (matches) {
|
||||
const [, searchType, packageName] = Array.from(matches)
|
||||
return {searchType, packageName}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
performSearch () {
|
||||
const query = this.refs.searchEditor.getText().trim().toLowerCase()
|
||||
if (query) {
|
||||
this.performSearchForQuery(query)
|
||||
}
|
||||
}
|
||||
|
||||
performSearchForQuery (query) {
|
||||
const gitUrlInfo = hostedGitInfo.fromUrl(query)
|
||||
if (gitUrlInfo) {
|
||||
const type = gitUrlInfo.default
|
||||
if (type === 'sshurl' || type === 'https' || type === 'shortcut') {
|
||||
this.showGitInstallPackageCard({name: query, gitUrlInfo})
|
||||
}
|
||||
} else {
|
||||
this.search(query)
|
||||
}
|
||||
}
|
||||
|
||||
showGitInstallPackageCard (pack) {
|
||||
if (this.currentGitPackageCard) {
|
||||
this.currentGitPackageCard.destroy()
|
||||
}
|
||||
|
||||
this.currentGitPackageCard = this.getPackageCardView(pack)
|
||||
this.currentGitPackageCard.displayGitPackageInstallInformation()
|
||||
this.replaceCurrentGitPackageCardView()
|
||||
}
|
||||
|
||||
updateGitPackageCard (pack) {
|
||||
if (this.currentGitPackageCard) {
|
||||
this.currentGitPackageCard.destroy()
|
||||
}
|
||||
|
||||
this.currentGitPackageCard = this.getPackageCardView(pack)
|
||||
this.replaceCurrentGitPackageCardView()
|
||||
}
|
||||
|
||||
replaceCurrentGitPackageCardView () {
|
||||
this.refs.resultsContainer.innerHTML = ''
|
||||
this.addPackageCardView(this.refs.resultsContainer, this.currentGitPackageCard)
|
||||
}
|
||||
|
||||
async search (query) {
|
||||
this.refs.resultsContainer.innerHTML = ''
|
||||
this.refs.searchMessage.textContent = `Searching ${this.searchType} for \u201C${query}\u201D\u2026`
|
||||
this.refs.searchMessage.style.display = ''
|
||||
|
||||
const options = {}
|
||||
options[this.searchType] = true
|
||||
|
||||
try {
|
||||
const packages = (await this.client.search(query, options)) || []
|
||||
this.refs.resultsContainer.innerHTML = ''
|
||||
this.refs.searchMessage.style.display = 'none'
|
||||
if (packages.length === 0) {
|
||||
this.refs.searchMessage.textContent = `No ${this.searchType.replace(/s$/, '')} results for \u201C${query}\u201D`
|
||||
this.refs.searchMessage.style.display = ''
|
||||
}
|
||||
|
||||
this.addPackageViews(this.refs.resultsContainer, packages)
|
||||
} catch (error) {
|
||||
this.refs.searchMessage.style.display = 'none'
|
||||
this.refs.searchErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
}
|
||||
}
|
||||
|
||||
addPackageViews (container, packages) {
|
||||
for (const pack of packages) {
|
||||
this.addPackageCardView(container, this.getPackageCardView(pack))
|
||||
}
|
||||
}
|
||||
|
||||
addPackageCardView (container, packageCard) {
|
||||
const packageRow = document.createElement('div')
|
||||
packageRow.classList.add('row')
|
||||
packageRow.appendChild(packageCard.element)
|
||||
container.appendChild(packageRow)
|
||||
}
|
||||
|
||||
getPackageCardView (pack) {
|
||||
return new PackageCard(pack, this.settingsView, this.packageManager, {back: 'Install'})
|
||||
}
|
||||
|
||||
filterPackages (packages, themes) {
|
||||
return packages.filter(({theme}) => themes ? theme : !theme)
|
||||
}
|
||||
|
||||
// Load and display the featured packages that are available to install.
|
||||
loadFeaturedPackages (loadThemes) {
|
||||
if (loadThemes == null) {
|
||||
loadThemes = false
|
||||
}
|
||||
this.refs.featuredContainer.innerHTML = ''
|
||||
|
||||
if (loadThemes) {
|
||||
this.refs.installHeading.textContent = 'Install Themes'
|
||||
this.refs.featuredHeading.textContent = 'Featured Themes'
|
||||
this.refs.loadingMessage.textContent = 'Loading featured themes\u2026'
|
||||
} else {
|
||||
this.refs.installHeading.textContent = 'Install Packages'
|
||||
this.refs.featuredHeading.textContent = 'Featured Packages'
|
||||
this.refs.loadingMessage.textContent = 'Loading featured packages\u2026'
|
||||
}
|
||||
|
||||
this.refs.loadingMessage.style.display = ''
|
||||
|
||||
const handle = error => {
|
||||
this.refs.loadingMessage.style.display = 'none'
|
||||
this.refs.featuredErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
}
|
||||
|
||||
if (loadThemes) {
|
||||
this.client.featuredThemes((error, themes) => {
|
||||
if (error) {
|
||||
handle(error)
|
||||
} else {
|
||||
this.refs.loadingMessage.style.display = 'none'
|
||||
this.refs.featuredHeading.textContent = 'Featured Themes'
|
||||
this.addPackageViews(this.refs.featuredContainer, themes)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.client.featuredPackages((error, packages) => {
|
||||
if (error) {
|
||||
handle(error)
|
||||
} else {
|
||||
this.refs.loadingMessage.style.display = 'none'
|
||||
this.refs.featuredHeading.textContent = 'Featured Packages'
|
||||
this.addPackageViews(this.refs.featuredContainer, packages)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didClickOpenAtomIo (event) {
|
||||
event.preventDefault()
|
||||
electron.shell.openExternal(this.atomIoURL)
|
||||
}
|
||||
|
||||
didClickSearchPackagesButton () {
|
||||
if (!this.refs.searchPackagesButton.classList.contains('selected')) {
|
||||
this.setSearchType('package')
|
||||
}
|
||||
|
||||
this.performSearch()
|
||||
}
|
||||
|
||||
didClickSearchThemesButton () {
|
||||
if (!this.refs.searchThemesButton.classList.contains('selected')) {
|
||||
this.setSearchType('theme')
|
||||
}
|
||||
|
||||
this.performSearch()
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
321
packages/settings-view/lib/installed-packages-panel.js
Normal file
321
packages/settings-view/lib/installed-packages-panel.js
Normal file
@ -0,0 +1,321 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable, TextEditor} from 'atom'
|
||||
import etch from 'etch'
|
||||
import fuzzaldrin from 'fuzzaldrin'
|
||||
|
||||
import CollapsibleSectionPanel from './collapsible-section-panel'
|
||||
import PackageCard from './package-card'
|
||||
import ErrorView from './error-view'
|
||||
|
||||
import List from './list'
|
||||
import ListView from './list-view'
|
||||
import {ownerFromRepository, packageComparatorAscending} from './utils'
|
||||
|
||||
export default class InstalledPackagesPanel extends CollapsibleSectionPanel {
|
||||
static loadPackagesDelay () {
|
||||
return 300
|
||||
}
|
||||
|
||||
constructor (settingsView, packageManager) {
|
||||
super()
|
||||
etch.initialize(this)
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
this.items = {
|
||||
dev: new List('name'),
|
||||
core: new List('name'),
|
||||
user: new List('name'),
|
||||
git: new List('name')
|
||||
}
|
||||
this.itemViews = {
|
||||
dev: new ListView(this.items.dev, this.refs.devPackages, this.createPackageCard.bind(this)),
|
||||
core: new ListView(this.items.core, this.refs.corePackages, this.createPackageCard.bind(this)),
|
||||
user: new ListView(this.items.user, this.refs.communityPackages, this.createPackageCard.bind(this)),
|
||||
git: new ListView(this.items.git, this.refs.gitPackages, this.createPackageCard.bind(this))
|
||||
}
|
||||
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions.add(
|
||||
this.refs.filterEditor.onDidStopChanging(() => { this.matchPackages() })
|
||||
)
|
||||
this.subscriptions.add(
|
||||
this.packageManager.on('package-install-failed theme-install-failed package-uninstall-failed theme-uninstall-failed package-update-failed theme-update-failed', ({pack, error}) => {
|
||||
this.refs.updateErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
})
|
||||
)
|
||||
|
||||
let loadPackagesTimeout
|
||||
this.subscriptions.add(
|
||||
this.packageManager.on('package-updated package-installed package-uninstalled', () => {
|
||||
clearTimeout(loadPackagesTimeout)
|
||||
loadPackagesTimeout = setTimeout(this.loadPackages.bind(this), InstalledPackagesPanel.loadPackagesDelay())
|
||||
})
|
||||
)
|
||||
|
||||
this.subscriptions.add(this.handleEvents())
|
||||
this.subscriptions.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
this.loadPackages()
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.filterEditor.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='panels-item' tabIndex='-1'>
|
||||
<section className='section'>
|
||||
<div className='section-container'>
|
||||
<div className='section-heading icon icon-package'>
|
||||
Installed Packages
|
||||
<span ref='totalPackages' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</div>
|
||||
<div className='editor-container'>
|
||||
<TextEditor ref='filterEditor' mini placeholderText='Filter packages by name' />
|
||||
</div>
|
||||
|
||||
<div ref='updateErrors' />
|
||||
|
||||
<section className='sub-section installed-packages'>
|
||||
<h3 ref='communityPackagesHeader' className='sub-section-heading icon icon-package'>
|
||||
Community Packages
|
||||
<span ref='communityCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='communityPackages' className='container package-container'>
|
||||
<div ref='communityLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading packages…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section core-packages'>
|
||||
<h3 ref='corePackagesHeader' className='sub-section-heading icon icon-package'>
|
||||
Core Packages
|
||||
<span ref='coreCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='corePackages' className='container package-container'>
|
||||
<div ref='coreLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading packages…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section dev-packages'>
|
||||
<h3 ref='devPackagesHeader' className='sub-section-heading icon icon-package'>
|
||||
Development Packages
|
||||
<span ref='devCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='devPackages' className='container package-container'>
|
||||
<div ref='devLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading packages…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section git-packages'>
|
||||
<h3 ref='gitPackagesHeader' className='sub-section-heading icon icon-package'>
|
||||
Git Packages
|
||||
<span ref='gitCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='gitPackages' className='container package-container'>
|
||||
<div ref='gitLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading packages…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
filterPackages (packages) {
|
||||
packages.dev = packages.dev.filter(({theme}) => !theme)
|
||||
packages.user = packages.user.filter(({theme}) => !theme)
|
||||
packages.core = packages.core.filter(({theme}) => !theme)
|
||||
packages.git = (packages.git || []).filter(({theme}) => !theme)
|
||||
|
||||
for (let pack of packages.core) {
|
||||
if (pack.repository == null) {
|
||||
pack.repository = `https://github.com/atom/${pack.name}`
|
||||
}
|
||||
}
|
||||
|
||||
for (let packageType of ['dev', 'core', 'user', 'git']) {
|
||||
for (let pack of packages[packageType]) {
|
||||
pack.owner = ownerFromRepository(pack.repository)
|
||||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
sortPackages (packages) {
|
||||
packages.dev.sort(packageComparatorAscending)
|
||||
packages.core.sort(packageComparatorAscending)
|
||||
packages.user.sort(packageComparatorAscending)
|
||||
packages.git.sort(packageComparatorAscending)
|
||||
return packages
|
||||
}
|
||||
|
||||
loadPackages () {
|
||||
const packagesWithUpdates = {}
|
||||
this.packageManager.getOutdated().then((packages) => {
|
||||
for (let {name, latestVersion} of packages) {
|
||||
packagesWithUpdates[name] = latestVersion
|
||||
}
|
||||
this.displayPackageUpdates(packagesWithUpdates)
|
||||
})
|
||||
|
||||
this.packageManager.getInstalled().then((packages) => {
|
||||
this.packages = this.sortPackages(this.filterPackages(packages))
|
||||
this.refs.devLoadingArea.remove()
|
||||
this.items.dev.setItems(this.packages.dev)
|
||||
|
||||
this.refs.coreLoadingArea.remove()
|
||||
this.items.core.setItems(this.packages.core)
|
||||
|
||||
this.refs.communityLoadingArea.remove()
|
||||
this.items.user.setItems(this.packages.user)
|
||||
|
||||
this.refs.gitLoadingArea.remove()
|
||||
this.items.git.setItems(this.packages.git)
|
||||
|
||||
// TODO show empty mesage per section
|
||||
|
||||
this.updateSectionCounts()
|
||||
this.displayPackageUpdates(packagesWithUpdates)
|
||||
|
||||
this.matchPackages()
|
||||
}).catch((error) => {
|
||||
console.error(error.message, error.stack)
|
||||
})
|
||||
}
|
||||
|
||||
displayPackageUpdates (packagesWithUpdates) {
|
||||
for (const packageType of ['dev', 'core', 'user', 'git']) {
|
||||
for (const packageCard of this.itemViews[packageType].getViews()) {
|
||||
const newVersion = packagesWithUpdates[packageCard.pack.name]
|
||||
if (newVersion) {
|
||||
packageCard.displayAvailableUpdate(newVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPackageCard (pack) {
|
||||
return new PackageCard(pack, this.settingsView, this.packageManager, {back: 'Packages'})
|
||||
}
|
||||
|
||||
filterPackageListByText (text) {
|
||||
if (!this.packages) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let packageType of ['dev', 'core', 'user', 'git']) {
|
||||
const allViews = this.itemViews[packageType].getViews()
|
||||
const activeViews = this.itemViews[packageType].filterViews((pack) => {
|
||||
if (text === '') {
|
||||
return true
|
||||
} else {
|
||||
const owner = pack.owner != null ? pack.owner : ownerFromRepository(pack.repository)
|
||||
const filterText = `${pack.name} ${owner}`
|
||||
return fuzzaldrin.score(filterText, text) > 0
|
||||
}
|
||||
})
|
||||
|
||||
for (const view of allViews) {
|
||||
if (view) {
|
||||
view.element.style.display = 'none'
|
||||
view.element.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
for (const view of activeViews) {
|
||||
if (view) {
|
||||
view.element.style.display = ''
|
||||
view.element.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSectionCounts()
|
||||
}
|
||||
|
||||
updateUnfilteredSectionCounts () {
|
||||
this.updateSectionCount(this.refs.communityPackagesHeader, this.refs.communityCount, this.packages.user.length)
|
||||
this.updateSectionCount(this.refs.corePackagesHeader, this.refs.coreCount, this.packages.core.length)
|
||||
this.updateSectionCount(this.refs.devPackagesHeader, this.refs.devCount, this.packages.dev.length)
|
||||
this.updateSectionCount(this.refs.gitPackagesHeader, this.refs.gitCount, this.packages.git.length)
|
||||
|
||||
const totalPackages =
|
||||
this.packages.user.length +
|
||||
this.packages.core.length +
|
||||
this.packages.dev.length +
|
||||
this.packages.git.length
|
||||
this.refs.totalPackages.textContent = totalPackages.toString()
|
||||
}
|
||||
|
||||
updateFilteredSectionCounts () {
|
||||
const community = this.notHiddenCardsLength(this.refs.communityPackages)
|
||||
this.updateSectionCount(this.refs.communityPackagesHeader, this.refs.communityCount, community, this.packages.user.length)
|
||||
|
||||
const core = this.notHiddenCardsLength(this.refs.corePackages)
|
||||
this.updateSectionCount(this.refs.corePackagesHeader, this.refs.coreCount, core, this.packages.core.length)
|
||||
|
||||
const dev = this.notHiddenCardsLength(this.refs.devPackages)
|
||||
this.updateSectionCount(this.refs.devPackagesHeader, this.refs.devCount, dev, this.packages.dev.length)
|
||||
|
||||
const git = this.notHiddenCardsLength(this.refs.gitPackages)
|
||||
this.updateSectionCount(this.refs.gitPackagesHeader, this.refs.gitCount, git, this.packages.git.length)
|
||||
|
||||
const shownPackages = dev + core + community + git
|
||||
const totalPackages = this.packages.user.length + this.packages.core.length + this.packages.dev.length + this.packages.git.length
|
||||
this.refs.totalPackages.textContent = `${shownPackages}/${totalPackages}`
|
||||
}
|
||||
|
||||
resetSectionHasItems () {
|
||||
this.resetCollapsibleSections([this.refs.communityPackagesHeader, this.refs.corePackagesHeader, this.refs.devPackagesHeader, this.refs.gitPackagesHeader])
|
||||
}
|
||||
|
||||
matchPackages () {
|
||||
this.filterPackageListByText(this.refs.filterEditor.getText())
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
249
packages/settings-view/lib/keybindings-panel.js
Normal file
249
packages/settings-view/lib/keybindings-panel.js
Normal file
@ -0,0 +1,249 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable, TextEditor} from 'atom'
|
||||
import etch from 'etch'
|
||||
import _ from 'underscore-plus'
|
||||
import path from 'path'
|
||||
|
||||
export default class KeybindingsPanel {
|
||||
constructor () {
|
||||
etch.initialize(this)
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
this.otherPlatformPattern = new RegExp(`\\.platform-(?!${_.escapeRegExp(process.platform)}\\b)`)
|
||||
this.platformPattern = new RegExp(`\\.platform-${_.escapeRegExp(process.platform)}\\b`)
|
||||
|
||||
this.disposables.add(this.refs.searchEditor.onDidStopChanging(() => {
|
||||
this.filterKeyBindings(this.keyBindings, this.refs.searchEditor.getText())
|
||||
}))
|
||||
|
||||
this.disposables.add(atom.keymaps.onDidReloadKeymap(() => { this.loadKeyBindings() }))
|
||||
this.disposables.add(atom.keymaps.onDidUnloadKeymap(() => { this.loadKeyBindings() }))
|
||||
this.loadKeyBindings()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='panels-item' tabIndex='-1'>
|
||||
<section className='keybinding-panel section'>
|
||||
<div className='section-heading icon icon-keyboard'>Keybindings</div>
|
||||
<div className='text native-key-bindings' tabIndex='-1'>
|
||||
<span className='icon icon-question' />
|
||||
<span>You can override these keybindings by copying </span>
|
||||
<span className='icon icon-clippy' />
|
||||
<span>and pasting them into </span>
|
||||
<a className='link' onclick={this.didClickOpenKeymapFile}>your keymap file</a>
|
||||
</div>
|
||||
|
||||
<div className='editor-container'>
|
||||
<TextEditor mini ref='searchEditor' placeholderText='Search keybindings' />
|
||||
</div>
|
||||
|
||||
<table className='native-key-bindings table text' tabIndex='-1'>
|
||||
<col className='keystroke' />
|
||||
<col className='command' />
|
||||
<col className='source' />
|
||||
<col className='selector' />
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='keystroke'>Keystroke</th>
|
||||
<th className='command'>Command</th>
|
||||
<th className='source'>Source</th>
|
||||
<th className='selector'>Selector</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ref='keybindingRows' />
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
loadKeyBindings () {
|
||||
this.refs.keybindingRows.innerHTML = ''
|
||||
this.keyBindings = _.sortBy(atom.keymaps.getKeyBindings(), 'keystrokes')
|
||||
this.appendKeyBindings(this.keyBindings)
|
||||
this.filterKeyBindings(this.keyBindings, this.refs.searchEditor.getText())
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.searchEditor.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
filterKeyBindings (keyBindings, filterString) {
|
||||
this.refs.keybindingRows.innerHTML = ''
|
||||
for (let keyBinding of keyBindings) {
|
||||
let {selector, keystrokes, command, source} = keyBinding
|
||||
source = KeybindingsPanel.determineSource(source)
|
||||
var searchString = `${selector}${keystrokes}${command}${source}`.toLowerCase()
|
||||
if (!searchString) {
|
||||
continue
|
||||
}
|
||||
|
||||
const keywords = filterString.trim().toLowerCase().split(' ')
|
||||
if (keywords.every(keyword => searchString.indexOf(keyword) !== -1)) {
|
||||
this.appendKeyBinding(keyBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendKeyBindings (keyBindings) {
|
||||
for (const keyBinding of keyBindings) {
|
||||
this.appendKeyBinding(keyBinding)
|
||||
}
|
||||
}
|
||||
|
||||
appendKeyBinding (keyBinding) {
|
||||
if (!this.showSelector(keyBinding.selector)) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = this.elementForKeyBinding(keyBinding)
|
||||
element.dataset.keyBinding = keyBinding
|
||||
this.refs.keybindingRows.appendChild(element)
|
||||
}
|
||||
|
||||
showSelector (selector) {
|
||||
let segments
|
||||
if (selector) {
|
||||
segments = selector.split(',') || []
|
||||
} else {
|
||||
segments = []
|
||||
}
|
||||
|
||||
return segments.some((s) => this.platformPattern.test(s) || !this.otherPlatformPattern.test(s))
|
||||
}
|
||||
|
||||
elementForKeyBinding (keyBinding) {
|
||||
let {selector, keystrokes, command, source} = keyBinding
|
||||
source = KeybindingsPanel.determineSource(source)
|
||||
|
||||
const tr = document.createElement('tr')
|
||||
if (source === 'User') {
|
||||
tr.classList.add('is-user')
|
||||
}
|
||||
|
||||
const keystrokeTd = document.createElement('td')
|
||||
keystrokeTd.classList.add('keystroke')
|
||||
|
||||
const copyIcon = document.createElement('span')
|
||||
copyIcon.classList.add('icon', 'icon-clippy', 'copy-icon')
|
||||
copyIcon.onclick = () => {
|
||||
let content
|
||||
const keymapExtension = path.extname(atom.keymaps.getUserKeymapPath())
|
||||
|
||||
const escapeCSON = (input) => {
|
||||
return JSON.stringify(input)
|
||||
.slice(1, -1) // Remove wrapping double quotes
|
||||
.replace(/\\"/g, '"') // Unescape double quotes
|
||||
.replace(/'/g, '\\\'') // Escape single quotes
|
||||
}
|
||||
|
||||
if (keymapExtension === '.cson') {
|
||||
content = `'${escapeCSON(selector)}':\n '${escapeCSON(keystrokes)}': '${escapeCSON(command)}'`
|
||||
} else {
|
||||
content = `${JSON.stringify(selector)}: {\n ${JSON.stringify(keystrokes)}: ${JSON.stringify(command)}\n}`
|
||||
}
|
||||
return atom.clipboard.write(content)
|
||||
}
|
||||
keystrokeTd.appendChild(copyIcon)
|
||||
|
||||
const keystrokesSpan = document.createElement('span')
|
||||
keystrokesSpan.textContent = keystrokes
|
||||
keystrokeTd.appendChild(keystrokesSpan)
|
||||
tr.appendChild(keystrokeTd)
|
||||
|
||||
const commandTd = document.createElement('td')
|
||||
commandTd.classList.add('command')
|
||||
commandTd.textContent = command
|
||||
tr.appendChild(commandTd)
|
||||
|
||||
const sourceTd = document.createElement('td')
|
||||
sourceTd.classList.add('source')
|
||||
sourceTd.textContent = source
|
||||
tr.appendChild(sourceTd)
|
||||
|
||||
const selectorTd = document.createElement('td')
|
||||
selectorTd.classList.add('selector')
|
||||
selectorTd.textContent = selector
|
||||
tr.appendChild(selectorTd)
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
didClickOpenKeymapFile (e) {
|
||||
e.preventDefault()
|
||||
atom.commands.dispatch(atom.views.getView(atom.workspace), 'application:open-your-keymap')
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
|
||||
// Private: Returns a user friendly description of where a keybinding was
|
||||
// loaded from.
|
||||
//
|
||||
// * filePath:
|
||||
// The absolute path from which the keymap was loaded
|
||||
//
|
||||
// Returns one of:
|
||||
// * `Core` indicates it comes from a bundled package.
|
||||
// * `User` indicates that it was defined by a user.
|
||||
// * `<package-name>` the package which defined it.
|
||||
// * `Unknown` if an invalid path was passed in.
|
||||
static determineSource (filePath) {
|
||||
if (!filePath) {
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
if (filePath.indexOf(path.join(atom.getLoadSettings().resourcePath, 'keymaps')) === 0) {
|
||||
return 'Core'
|
||||
} else if (filePath === atom.keymaps.getUserKeymapPath()) {
|
||||
return 'User'
|
||||
} else {
|
||||
const pathParts = filePath.split(path.sep)
|
||||
const packageNameIndex = pathParts.length - 3
|
||||
const packageName = pathParts[packageNameIndex] != null ? pathParts[packageNameIndex] : ''
|
||||
return _.undasherize(_.uncamelcase(packageName))
|
||||
}
|
||||
}
|
||||
}
|
54
packages/settings-view/lib/list-view.js
Normal file
54
packages/settings-view/lib/list-view.js
Normal file
@ -0,0 +1,54 @@
|
||||
module.exports =
|
||||
class ListView {
|
||||
// * `list` a {List} object
|
||||
// * `container` a jQuery element
|
||||
// * `createView` a Function that returns a jQuery element / HTMLElement
|
||||
// * `item` the item to create the view for
|
||||
constructor (list, container, createView) {
|
||||
this.list = list
|
||||
this.container = container
|
||||
this.createView = createView
|
||||
this.views = []
|
||||
this.viewMap = {}
|
||||
this.list.onDidAddItem(item => this.addView(item))
|
||||
this.list.onDidRemoveItem(item => this.removeView(item))
|
||||
this.addViews()
|
||||
}
|
||||
|
||||
getViews () {
|
||||
return this.views
|
||||
}
|
||||
|
||||
filterViews (filterFn) {
|
||||
return this.list.filterItems(filterFn).map((item) => this.viewMap[this.list.keyForItem(item)])
|
||||
}
|
||||
|
||||
addViews () {
|
||||
for (const item of this.list.getItems()) {
|
||||
this.addView(item)
|
||||
}
|
||||
}
|
||||
|
||||
addView (item) {
|
||||
const view = this.createView(item)
|
||||
this.views.push(view)
|
||||
this.viewMap[this.list.keyForItem(item)] = view
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.classList.add('row')
|
||||
row.appendChild(view.element)
|
||||
this.container.insertBefore(row, this.container.children[0])
|
||||
}
|
||||
|
||||
removeView (item) {
|
||||
const key = this.list.keyForItem(item)
|
||||
const view = this.viewMap[key]
|
||||
if (view) {
|
||||
const index = this.views.indexOf(view)
|
||||
if (index > -1) this.views.splice(index, 1)
|
||||
delete this.viewMap[key]
|
||||
view.element.parentElement.remove()
|
||||
view.destroy()
|
||||
}
|
||||
}
|
||||
}
|
71
packages/settings-view/lib/list.js
Normal file
71
packages/settings-view/lib/list.js
Normal file
@ -0,0 +1,71 @@
|
||||
const {Emitter} = require('atom')
|
||||
|
||||
module.exports =
|
||||
class List {
|
||||
constructor (key) {
|
||||
this.key = key
|
||||
this.items = []
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
getItems () {
|
||||
return this.items
|
||||
}
|
||||
|
||||
filterItems (filterFn) {
|
||||
const result = []
|
||||
for (const item of this.items) {
|
||||
if (filterFn(item)) {
|
||||
result.push(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
keyForItem (item) {
|
||||
return item[this.key]
|
||||
}
|
||||
|
||||
setItems (items) {
|
||||
items = items.slice()
|
||||
const setToAdd = difference(items, this.items, this.key)
|
||||
const setToRemove = difference(this.items, items, this.key)
|
||||
|
||||
this.items = items
|
||||
|
||||
for (const item of setToAdd) {
|
||||
this.emitter.emit('did-add-item', item)
|
||||
}
|
||||
|
||||
for (const item of setToRemove) {
|
||||
this.emitter.emit('did-remove-item', item)
|
||||
}
|
||||
}
|
||||
|
||||
onDidAddItem (callback) {
|
||||
return this.emitter.on('did-add-item', callback)
|
||||
}
|
||||
|
||||
onDidRemoveItem (callback) {
|
||||
return this.emitter.on('did-remove-item', callback)
|
||||
}
|
||||
}
|
||||
|
||||
const difference = (array1, array2, key) => {
|
||||
const obj1 = {}
|
||||
for (const item of array1) {
|
||||
obj1[item[key]] = item
|
||||
}
|
||||
|
||||
const obj2 = {}
|
||||
for (const item of array2) {
|
||||
obj2[item[key]] = item
|
||||
}
|
||||
|
||||
const diff = []
|
||||
for (const k in obj1) {
|
||||
const v = obj1[k]
|
||||
if (obj2[k] == null) diff.push(v)
|
||||
}
|
||||
return diff
|
||||
}
|
96
packages/settings-view/lib/main.js
Normal file
96
packages/settings-view/lib/main.js
Normal file
@ -0,0 +1,96 @@
|
||||
let SettingsView = null
|
||||
let settingsView = null
|
||||
|
||||
let statusView = null
|
||||
|
||||
const PackageManager = require('./package-manager')
|
||||
let packageManager = null
|
||||
|
||||
const SnippetsProvider = {
|
||||
getSnippets () { return atom.config.scopedSettingsStore.propertySets }
|
||||
}
|
||||
|
||||
const CONFIG_URI = 'atom://config'
|
||||
|
||||
module.exports = {
|
||||
handleURI (parsed) {
|
||||
switch (parsed.pathname) {
|
||||
case '/show-package': this.showPackage(parsed.query.package)
|
||||
}
|
||||
},
|
||||
|
||||
showPackage (packageName) {
|
||||
atom.workspace.open(`atom://config/packages/${packageName}`)
|
||||
},
|
||||
|
||||
activate () {
|
||||
atom.workspace.addOpener(uri => {
|
||||
if (uri.startsWith(CONFIG_URI)) {
|
||||
if (settingsView == null || settingsView.destroyed) {
|
||||
settingsView = this.createSettingsView({uri})
|
||||
} else {
|
||||
const pane = atom.workspace.paneForItem(settingsView)
|
||||
if (pane) pane.activate()
|
||||
}
|
||||
|
||||
settingsView.showPanelForURI(uri)
|
||||
return settingsView
|
||||
}
|
||||
})
|
||||
|
||||
atom.commands.add('atom-workspace', {
|
||||
'settings-view:open' () { atom.workspace.open(CONFIG_URI) },
|
||||
'settings-view:core' () { atom.workspace.open(`${CONFIG_URI}/core`) },
|
||||
'settings-view:editor' () { atom.workspace.open(`${CONFIG_URI}/editor`) },
|
||||
'settings-view:show-keybindings' () { atom.workspace.open(`${CONFIG_URI}/keybindings`) },
|
||||
'settings-view:change-themes' () { atom.workspace.open(`${CONFIG_URI}/themes`) },
|
||||
'settings-view:install-packages-and-themes' () { atom.workspace.open(`${CONFIG_URI}/install`) },
|
||||
'settings-view:view-installed-themes' () { atom.workspace.open(`${CONFIG_URI}/themes`) },
|
||||
'settings-view:uninstall-themes' () { atom.workspace.open(`${CONFIG_URI}/themes`) },
|
||||
'settings-view:view-installed-packages' () { atom.workspace.open(`${CONFIG_URI}/packages`) },
|
||||
'settings-view:uninstall-packages' () { atom.workspace.open(`${CONFIG_URI}/packages`) },
|
||||
'settings-view:check-for-package-updates' () { atom.workspace.open(`${CONFIG_URI}/updates`) }
|
||||
})
|
||||
|
||||
if (process.platform === 'win32' && require('atom').WinShell != null) {
|
||||
atom.commands.add('atom-workspace', {'settings-view:system' () { atom.workspace.open(`${CONFIG_URI}/system`) }})
|
||||
}
|
||||
},
|
||||
|
||||
deactivate () {
|
||||
if (settingsView) settingsView.destroy()
|
||||
if (statusView) statusView.destroy()
|
||||
settingsView = null
|
||||
packageManager = null
|
||||
statusView = null
|
||||
},
|
||||
|
||||
consumeStatusBar (statusBar) {
|
||||
if (packageManager == null) packageManager = new PackageManager()
|
||||
packageManager.getOutdated().then(updates => {
|
||||
if (packageManager) {
|
||||
const PackageUpdatesStatusView = require('./package-updates-status-view')
|
||||
statusView = new PackageUpdatesStatusView()
|
||||
statusView.initialize(statusBar, packageManager, updates)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
consumeSnippets (snippets) {
|
||||
if (typeof snippets.getUnparsedSnippets === 'function') {
|
||||
SnippetsProvider.getSnippets = snippets.getUnparsedSnippets.bind(snippets)
|
||||
}
|
||||
if (typeof snippets.getUserSnippetsPath === 'function') {
|
||||
SnippetsProvider.getUserSnippetsPath = snippets.getUserSnippetsPath.bind(snippets)
|
||||
}
|
||||
},
|
||||
|
||||
createSettingsView (params) {
|
||||
if (SettingsView == null) SettingsView = require('./settings-view')
|
||||
if (packageManager == null) packageManager = new PackageManager()
|
||||
params.packageManager = packageManager
|
||||
params.snippetsProvider = SnippetsProvider
|
||||
settingsView = new SettingsView(params)
|
||||
return settingsView
|
||||
}
|
||||
}
|
601
packages/settings-view/lib/package-card.js
Normal file
601
packages/settings-view/lib/package-card.js
Normal file
@ -0,0 +1,601 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
import {shell} from 'electron'
|
||||
import etch from 'etch'
|
||||
|
||||
import {ownerFromRepository} from './utils'
|
||||
|
||||
let marked = null
|
||||
|
||||
export default class PackageCard {
|
||||
constructor (pack, settingsView, packageManager, options = {}) {
|
||||
this.pack = pack
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
this.disposables = new CompositeDisposable()
|
||||
|
||||
// It might be useful to either wrap this.pack in a class that has a
|
||||
// ::validate method, or add a method here. At the moment I think all cases
|
||||
// of malformed package metadata are handled here and in ::content but belt
|
||||
// and suspenders, you know
|
||||
this.client = this.packageManager.getClient()
|
||||
this.type = this.pack.theme ? 'theme' : 'package'
|
||||
this.name = this.pack.name
|
||||
this.onSettingsView = options.onSettingsView
|
||||
|
||||
if (this.pack.latestVersion !== this.pack.version) {
|
||||
this.newVersion = this.pack.latestVersion
|
||||
}
|
||||
|
||||
if (this.pack.apmInstallSource && this.pack.apmInstallSource.type === 'git') {
|
||||
if (this.pack.apmInstallSource.sha !== this.pack.latestSha) {
|
||||
this.newSha = this.pack.latestSha
|
||||
}
|
||||
}
|
||||
|
||||
// Default to displaying the download count
|
||||
if (!options.stats) {
|
||||
options.stats = {downloads: true}
|
||||
}
|
||||
|
||||
etch.initialize(this)
|
||||
|
||||
this.displayStats(options)
|
||||
this.handlePackageEvents()
|
||||
this.handleButtonEvents(options)
|
||||
this.loadCachedMetadata()
|
||||
|
||||
// themes have no status and cannot be dis/enabled
|
||||
if (this.type === 'theme') {
|
||||
this.refs.statusIndicator.remove()
|
||||
this.refs.enablementButton.remove()
|
||||
}
|
||||
|
||||
if (atom.packages.isBundledPackage(this.pack.name)) {
|
||||
this.refs.installButtonGroup.remove()
|
||||
this.refs.uninstallButton.remove()
|
||||
}
|
||||
|
||||
if (!this.newVersion && !this.newSha) {
|
||||
this.refs.updateButtonGroup.style.display = 'none'
|
||||
}
|
||||
|
||||
this.hasCompatibleVersion = true
|
||||
this.updateInterfaceState()
|
||||
}
|
||||
|
||||
render () {
|
||||
const displayName = (this.pack.gitUrlInfo ? this.pack.gitUrlInfo.project : this.pack.name) || ''
|
||||
const owner = ownerFromRepository(this.pack.repository)
|
||||
const description = this.pack.description || ''
|
||||
|
||||
return (
|
||||
<div className='package-card col-lg-8'>
|
||||
<div ref='statsContainer' className='stats pull-right'>
|
||||
<span ref='packageStars' className='stats-item'>
|
||||
<span ref='stargazerIcon' className='icon icon-star' />
|
||||
<span ref='stargazerCount' className='value' />
|
||||
</span>
|
||||
|
||||
<span ref='packageDownloads' className='stats-item'>
|
||||
<span ref='downloadIcon' className='icon icon-cloud-download' />
|
||||
<span ref='downloadCount' className='value' />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='body'>
|
||||
<h4 className='card-name'>
|
||||
<a className='package-name' ref='packageName'>{displayName}</a>
|
||||
<span className='package-version'>
|
||||
<span ref='versionValue' className='value'>{String(this.pack.version)}</span>
|
||||
</span>
|
||||
</h4>
|
||||
<span ref='packageDescription' className='package-description'>{description}</span>
|
||||
<div ref='packageMessage' className='package-message' />
|
||||
</div>
|
||||
|
||||
<div className='meta'>
|
||||
<div ref='metaUserContainer' className='meta-user'>
|
||||
<a ref='avatarLink'>
|
||||
{/* A transparent gif so there is no "broken border" */}
|
||||
<img ref='avatar' className='avatar' src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' />
|
||||
</a>
|
||||
<a ref='loginLink' className='author'>{owner}</a>
|
||||
</div>
|
||||
<div className='meta-controls'>
|
||||
<div className='btn-toolbar'>
|
||||
<div ref='updateButtonGroup' className='btn-group'>
|
||||
<button type='button' className='btn btn-info icon icon-cloud-download install-button' ref='updateButton'>Update</button>
|
||||
</div>
|
||||
<div ref='installButtonGroup' className='btn-group'>
|
||||
<button type='button' className='btn btn-info icon icon-cloud-download install-button' ref='installButton'>Install</button>
|
||||
</div>
|
||||
<div ref='packageActionButtonGroup' className='btn-group'>
|
||||
<button type='button' className='btn icon icon-gear settings' ref='settingsButton'>Settings</button>
|
||||
<button type='button' className='btn icon icon-trashcan uninstall-button' ref='uninstallButton'>Uninstall</button>
|
||||
<button type='button' className='btn icon icon-playback-pause enablement' ref='enablementButton'>
|
||||
<span className='disable-text'>Disable</span>
|
||||
</button>
|
||||
<button type='button' className='btn status-indicator' tabIndex='-1' ref='statusIndicator' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
locateCompatiblePackageVersion (callback) {
|
||||
this.packageManager.loadCompatiblePackageVersion(this.pack.name, (err, pack) => {
|
||||
if (err != null) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
const packageVersion = pack.version
|
||||
|
||||
// A compatible version exist, we activate the install button and
|
||||
// set this.installablePack so that the install action installs the
|
||||
// compatible version of the package.
|
||||
if (packageVersion) {
|
||||
this.refs.versionValue.textContent = packageVersion
|
||||
if (packageVersion !== this.pack.version) {
|
||||
this.refs.versionValue.classList.add('text-warning')
|
||||
this.refs.packageMessage.classList.add('text-warning')
|
||||
this.refs.packageMessage.textContent = `Version ${packageVersion} is not the latest version available for this package, but it's the latest that is compatible with your version of Atom.`
|
||||
}
|
||||
|
||||
this.installablePack = pack
|
||||
this.hasCompatibleVersion = true
|
||||
} else {
|
||||
this.hasCompatibleVersion = false
|
||||
this.refs.versionValue.classList.add('text-error')
|
||||
this.refs.packageMessage.classList.add('text-error')
|
||||
this.refs.packageMessage.insertAdjacentText(
|
||||
'beforeend',
|
||||
`There's no version of this package that is compatible with your Atom version. The version must satisfy ${this.pack.engines.atom}.`
|
||||
)
|
||||
console.error(`No available version compatible with the installed Atom version: ${atom.getVersion()}`)
|
||||
}
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
handleButtonEvents (options) {
|
||||
if (options && options.onSettingsView) {
|
||||
this.refs.settingsButton.style.display = 'none'
|
||||
} else {
|
||||
const clickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
this.settingsView.showPanel(this.pack.name, {back: options ? options.back : null, pack: this.pack})
|
||||
}
|
||||
|
||||
this.element.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.element.removeEventListener('click', clickHandler) }))
|
||||
|
||||
this.refs.settingsButton.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.settingsButton.removeEventListener('click', clickHandler) }))
|
||||
}
|
||||
|
||||
const installButtonClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
this.install()
|
||||
}
|
||||
this.refs.installButton.addEventListener('click', installButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.installButton.removeEventListener('click', installButtonClickHandler) }))
|
||||
|
||||
const uninstallButtonClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
this.uninstall()
|
||||
}
|
||||
this.refs.uninstallButton.addEventListener('click', uninstallButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.uninstallButton.removeEventListener('click', uninstallButtonClickHandler) }))
|
||||
|
||||
const updateButtonClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
this.update().then(() => {
|
||||
let oldVersion = ''
|
||||
let newVersion = ''
|
||||
|
||||
if (this.pack.apmInstallSource && this.pack.apmInstallSource.type === 'git') {
|
||||
oldVersion = this.pack.apmInstallSource.sha.substr(0, 8)
|
||||
newVersion = `${this.pack.latestSha.substr(0, 8)}`
|
||||
} else if (this.pack.version && this.pack.latestVersion) {
|
||||
oldVersion = this.pack.version
|
||||
newVersion = this.pack.latestVersion
|
||||
}
|
||||
|
||||
let detail = ''
|
||||
if (oldVersion && newVersion) {
|
||||
detail = `${oldVersion} -> ${newVersion}`
|
||||
}
|
||||
|
||||
const notification = atom.notifications.addSuccess(`Restart Atom to complete the update of \`${this.pack.name}\`.`, {
|
||||
dismissable: true,
|
||||
buttons: [{
|
||||
text: 'Restart now',
|
||||
onDidClick () { return atom.restartApplication() }
|
||||
},
|
||||
{
|
||||
text: 'I\'ll do it later',
|
||||
onDidClick () { notification.dismiss() }
|
||||
}],
|
||||
detail
|
||||
})
|
||||
})
|
||||
}
|
||||
this.refs.updateButton.addEventListener('click', updateButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.updateButton.removeEventListener('click', updateButtonClickHandler) }))
|
||||
|
||||
const packageNameClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
shell.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`)
|
||||
}
|
||||
this.refs.packageName.addEventListener('click', packageNameClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.packageName.removeEventListener('click', packageNameClickHandler) }))
|
||||
|
||||
const packageAuthorClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
shell.openExternal(`https://pulsar-edit.dev/users/${ownerFromRepository(this.pack.repository)}`)
|
||||
}
|
||||
this.refs.loginLink.addEventListener('click', packageAuthorClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.loginLink.removeEventListener('click', packageAuthorClickHandler) }))
|
||||
this.refs.avatarLink.addEventListener('click', packageAuthorClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.avatarLink.removeEventListener('click', packageAuthorClickHandler) }))
|
||||
|
||||
const enablementButtonClickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
if (this.isDisabled()) {
|
||||
atom.packages.enablePackage(this.pack.name)
|
||||
} else {
|
||||
atom.packages.disablePackage(this.pack.name)
|
||||
}
|
||||
}
|
||||
this.refs.enablementButton.addEventListener('click', enablementButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.enablementButton.removeEventListener('click', enablementButtonClickHandler) }))
|
||||
|
||||
const packageMessageClickHandler = (event) => {
|
||||
const target = event.target.closest('a')
|
||||
if (target) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
if (target.href && target.href.startsWith('atom:')) {
|
||||
atom.workspace.open(target.href)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.refs.packageMessage.addEventListener('click', packageMessageClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.packageMessage.removeEventListener('click', packageMessageClickHandler) }))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
loadCachedMetadata () {
|
||||
this.client.avatar(ownerFromRepository(this.pack.repository), (err, avatarPath) => {
|
||||
if (!err && avatarPath) {
|
||||
this.refs.avatar.src = `file://${avatarPath}`
|
||||
}
|
||||
})
|
||||
|
||||
this.client.package(this.pack.name, (err, data) => {
|
||||
// We don't need to actually handle the error here, we can just skip
|
||||
// showing the download count if there's a problem.
|
||||
if (!err) {
|
||||
if (data == null) {
|
||||
data = {}
|
||||
}
|
||||
|
||||
if (this.pack.apmInstallSource && this.pack.apmInstallSource.type === 'git') {
|
||||
this.refs.downloadIcon.classList.remove('icon-cloud-download')
|
||||
this.refs.downloadIcon.classList.add('icon-git-branch')
|
||||
this.refs.downloadCount.textContent = this.pack.apmInstallSource.sha.substr(0, 8)
|
||||
} else {
|
||||
this.refs.stargazerCount.textContent = data.stargazers_count ? data.stargazers_count.toLocaleString() : ''
|
||||
this.refs.downloadCount.textContent = data.downloads ? data.downloads.toLocaleString() : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateInterfaceState () {
|
||||
this.refs.versionValue.textContent = (this.installablePack ? this.installablePack.version : null) || this.pack.version
|
||||
if (this.pack.apmInstallSource && this.pack.apmInstallSource.type === 'git') {
|
||||
this.refs.downloadCount.textContent = this.pack.apmInstallSource.sha.substr(0, 8)
|
||||
}
|
||||
|
||||
this.updateSettingsState()
|
||||
this.updateInstalledState()
|
||||
this.updateDisabledState()
|
||||
}
|
||||
|
||||
updateSettingsState () {
|
||||
if (this.hasSettings() && !this.onSettingsView) {
|
||||
this.refs.settingsButton.style.display = ''
|
||||
} else {
|
||||
this.refs.settingsButton.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// Section: disabled state updates
|
||||
|
||||
updateDisabledState () {
|
||||
if (this.isDisabled()) {
|
||||
this.displayDisabledState()
|
||||
} else if (this.element.classList.contains('disabled')) {
|
||||
this.displayEnabledState()
|
||||
}
|
||||
}
|
||||
|
||||
displayEnabledState () {
|
||||
this.element.classList.remove('disabled')
|
||||
if (this.type === 'theme') {
|
||||
this.refs.enablementButton.style.display = 'none'
|
||||
}
|
||||
this.refs.enablementButton.querySelector('.disable-text').textContent = 'Disable'
|
||||
this.refs.enablementButton.classList.add('icon-playback-pause')
|
||||
this.refs.enablementButton.classList.remove('icon-playback-play')
|
||||
this.refs.statusIndicator.classList.remove('is-disabled')
|
||||
}
|
||||
|
||||
displayDisabledState () {
|
||||
this.element.classList.add('disabled')
|
||||
this.refs.enablementButton.querySelector('.disable-text').textContent = 'Enable'
|
||||
this.refs.enablementButton.classList.add('icon-playback-play')
|
||||
this.refs.enablementButton.classList.remove('icon-playback-pause')
|
||||
this.refs.statusIndicator.classList.add('is-disabled')
|
||||
this.refs.enablementButton.disabled = false
|
||||
}
|
||||
|
||||
// Section: installed state updates
|
||||
|
||||
updateInstalledState () {
|
||||
if (this.isInstalled()) {
|
||||
this.displayInstalledState()
|
||||
} else {
|
||||
this.displayNotInstalledState()
|
||||
}
|
||||
}
|
||||
|
||||
displayInstalledState () {
|
||||
if (this.newVersion || this.newSha) {
|
||||
this.refs.updateButtonGroup.style.display = ''
|
||||
if (this.newVersion) {
|
||||
this.refs.updateButton.textContent = `Update to ${this.newVersion}`
|
||||
} else if (this.newSha) {
|
||||
this.refs.updateButton.textContent = `Update to ${this.newSha.substr(0, 8)}`
|
||||
}
|
||||
} else {
|
||||
this.refs.updateButtonGroup.style.display = 'none'
|
||||
}
|
||||
|
||||
this.refs.installButtonGroup.style.display = 'none'
|
||||
this.refs.packageActionButtonGroup.style.display = ''
|
||||
this.refs.uninstallButton.style.display = ''
|
||||
}
|
||||
|
||||
displayNotInstalledState () {
|
||||
this.refs.uninstallButton.style.display = 'none'
|
||||
const atomVersion = this.packageManager.normalizeVersion(atom.getVersion())
|
||||
if (!this.packageManager.satisfiesVersion(atomVersion, this.pack)) {
|
||||
this.hasCompatibleVersion = false
|
||||
this.setNotInstalledStateButtons()
|
||||
this.locateCompatiblePackageVersion(() => { this.setNotInstalledStateButtons() })
|
||||
} else {
|
||||
this.setNotInstalledStateButtons()
|
||||
}
|
||||
}
|
||||
|
||||
setNotInstalledStateButtons () {
|
||||
if (!this.hasCompatibleVersion) {
|
||||
this.refs.installButtonGroup.style.display = 'none'
|
||||
this.refs.updateButtonGroup.style.display = 'none'
|
||||
} else if (this.newVersion || this.newSha) {
|
||||
this.refs.updateButtonGroup.style.display = ''
|
||||
this.refs.installButtonGroup.style.display = 'none'
|
||||
} else {
|
||||
this.refs.updateButtonGroup.style.display = 'none'
|
||||
this.refs.installButtonGroup.style.display = ''
|
||||
}
|
||||
this.refs.packageActionButtonGroup.style.display = 'none'
|
||||
}
|
||||
|
||||
displayStats (options) {
|
||||
if (options && options.stats && options.stats.downloads) {
|
||||
this.refs.packageDownloads.style.display = ''
|
||||
} else {
|
||||
this.refs.packageDownloads.style.display = 'none'
|
||||
}
|
||||
|
||||
if (options && options.stats && options.stats.stars) {
|
||||
this.refs.packageStars.style.display = ''
|
||||
} else {
|
||||
this.refs.packageStars.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
displayGitPackageInstallInformation () {
|
||||
this.refs.metaUserContainer.remove()
|
||||
this.refs.statsContainer.remove()
|
||||
const {gitUrlInfo} = this.pack
|
||||
if (gitUrlInfo.default === 'shortcut') {
|
||||
this.refs.packageDescription.textContent = gitUrlInfo.https()
|
||||
} else {
|
||||
this.refs.packageDescription.textContent = gitUrlInfo.toString()
|
||||
}
|
||||
this.refs.installButton.classList.remove('icon-cloud-download')
|
||||
this.refs.installButton.classList.add('icon-git-commit')
|
||||
this.refs.updateButton.classList.remove('icon-cloud-download')
|
||||
this.refs.updateButton.classList.add('icon-git-commit')
|
||||
}
|
||||
|
||||
displayAvailableUpdate (newVersion) {
|
||||
this.newVersion = newVersion
|
||||
this.updateInterfaceState()
|
||||
}
|
||||
|
||||
handlePackageEvents () {
|
||||
this.disposables.add(atom.packages.onDidDeactivatePackage((pack) => {
|
||||
if (pack.name === this.pack.name) {
|
||||
this.updateDisabledState()
|
||||
}
|
||||
}))
|
||||
|
||||
this.disposables.add(atom.packages.onDidActivatePackage((pack) => {
|
||||
if (pack.name === this.pack.name) {
|
||||
this.updateDisabledState()
|
||||
}
|
||||
}))
|
||||
|
||||
this.disposables.add(atom.config.onDidChange('core.disabledPackages', () => {
|
||||
this.updateDisabledState()
|
||||
}))
|
||||
|
||||
this.subscribeToPackageEvent('package-installing theme-installing', () => {
|
||||
this.updateInterfaceState()
|
||||
this.refs.installButton.disabled = true
|
||||
this.refs.installButton.classList.add('is-installing')
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-updating theme-updating', () => {
|
||||
this.updateInterfaceState()
|
||||
this.refs.updateButton.disabled = true
|
||||
this.refs.updateButton.classList.add('is-installing')
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-uninstalling theme-uninstalling', () => {
|
||||
this.updateInterfaceState()
|
||||
this.refs.enablementButton.disabled = true
|
||||
this.refs.uninstallButton.disabled = true
|
||||
this.refs.uninstallButton.classList.add('is-uninstalling')
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-installed package-install-failed theme-installed theme-install-failed', () => {
|
||||
const loadedPack = atom.packages.getLoadedPackage(this.pack.name)
|
||||
const version = loadedPack && loadedPack.metadata ? loadedPack.metadata.version : null
|
||||
if (version) {
|
||||
this.pack.version = version
|
||||
}
|
||||
this.refs.installButton.disabled = false
|
||||
this.refs.installButton.classList.remove('is-installing')
|
||||
this.updateInterfaceState()
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-updated theme-updated', () => {
|
||||
const loadedPack = atom.packages.getLoadedPackage(this.pack.name)
|
||||
const metadata = loadedPack ? loadedPack.metadata : null
|
||||
if (metadata && metadata.version) {
|
||||
this.pack.version = metadata.version
|
||||
}
|
||||
|
||||
if (metadata && metadata.apmInstallSource) {
|
||||
this.pack.apmInstallSource = metadata.apmInstallSource
|
||||
}
|
||||
|
||||
this.newVersion = null
|
||||
this.newSha = null
|
||||
this.refs.updateButton.disabled = false
|
||||
this.refs.updateButton.classList.remove('is-installing')
|
||||
this.updateInterfaceState()
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-update-failed theme-update-failed', () => {
|
||||
this.refs.updateButton.disabled = false
|
||||
this.refs.updateButton.classList.remove('is-installing')
|
||||
this.updateInterfaceState()
|
||||
})
|
||||
|
||||
this.subscribeToPackageEvent('package-uninstalled package-uninstall-failed theme-uninstalled theme-uninstall-failed', () => {
|
||||
this.newVersion = null
|
||||
this.newSha = null
|
||||
this.refs.enablementButton.disabled = false
|
||||
this.refs.uninstallButton.disabled = false
|
||||
this.refs.uninstallButton.classList.remove('is-uninstalling')
|
||||
this.updateInterfaceState()
|
||||
})
|
||||
}
|
||||
|
||||
isInstalled () {
|
||||
return this.packageManager.isPackageInstalled(this.pack.name)
|
||||
}
|
||||
|
||||
isDisabled () {
|
||||
return atom.packages.isPackageDisabled(this.pack.name)
|
||||
}
|
||||
|
||||
hasSettings () {
|
||||
return this.packageManager.packageHasSettings(this.pack.name)
|
||||
}
|
||||
|
||||
subscribeToPackageEvent (event, callback) {
|
||||
this.disposables.add(this.packageManager.on(event, ({pack, error}) => {
|
||||
if (pack.pack != null) {
|
||||
pack = pack.pack
|
||||
}
|
||||
|
||||
const packageName = pack.name
|
||||
if (packageName === this.pack.name) {
|
||||
callback(pack, error)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Methods that should be on a Package model
|
||||
*/
|
||||
|
||||
install () {
|
||||
this.packageManager.install(this.installablePack != null ? this.installablePack : this.pack, (error) => {
|
||||
if (error != null) {
|
||||
console.error(`Installing ${this.type} ${this.pack.name} failed`, error.stack != null ? error.stack : error, error.stderr)
|
||||
} else {
|
||||
// if a package was disabled before installing it, re-enable it
|
||||
if (this.isDisabled()) {
|
||||
atom.packages.enablePackage(this.pack.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
update () {
|
||||
if (!this.newVersion && !this.newSha) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const pack = this.installablePack != null ? this.installablePack : this.pack
|
||||
const version = this.newVersion ? `v${this.newVersion}` : `#${this.newSha.substr(0, 8)}`
|
||||
return new Promise((resolve, reject) => {
|
||||
this.packageManager.update(pack, this.newVersion, error => {
|
||||
if (error != null) {
|
||||
atom.assert(false, 'Package update failed', assertionError => {
|
||||
assertionError.metadata = {
|
||||
type: this.type,
|
||||
name: pack.name,
|
||||
version,
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
errorStderr: error.stderr
|
||||
}
|
||||
})
|
||||
console.error(`Updating ${this.type} ${pack.name} to ${version} failed:\n`, error, error.stderr != null ? error.stderr : '')
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
uninstall () {
|
||||
this.packageManager.uninstall(this.pack, (error) => {
|
||||
if (error != null) {
|
||||
console.error(`Uninstalling ${this.type} ${this.pack.name} failed`, error.stack != null ? error.stack : error, error.stderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
517
packages/settings-view/lib/package-detail-view.js
Normal file
517
packages/settings-view/lib/package-detail-view.js
Normal file
@ -0,0 +1,517 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import path from 'path'
|
||||
import url from 'url'
|
||||
|
||||
import _ from 'underscore-plus'
|
||||
import fs from 'fs-plus'
|
||||
import {shell} from 'electron'
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
|
||||
import PackageCard from './package-card'
|
||||
import PackageGrammarsView from './package-grammars-view'
|
||||
import PackageKeymapView from './package-keymap-view'
|
||||
import PackageReadmeView from './package-readme-view'
|
||||
import PackageSnippetsView from './package-snippets-view'
|
||||
import SettingsPanel from './settings-panel'
|
||||
|
||||
const NORMALIZE_PACKAGE_DATA_README_ERROR = 'ERROR: No README data found!'
|
||||
|
||||
export default class PackageDetailView {
|
||||
constructor (pack, settingsView, packageManager, snippetsProvider) {
|
||||
this.pack = pack
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
this.snippetsProvider = snippetsProvider
|
||||
this.disposables = new CompositeDisposable()
|
||||
etch.initialize(this)
|
||||
this.loadPackage()
|
||||
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
const packageRepoClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
const repoUrl = this.packageManager.getRepositoryUrl(this.pack)
|
||||
if (typeof repoUrl === 'string') {
|
||||
if (url.parse(repoUrl).pathname === '/atom/atom') {
|
||||
shell.openExternal(`${repoUrl}/tree/master/packages/${this.pack.name}`)
|
||||
} else {
|
||||
shell.openExternal(repoUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.refs.packageRepo.addEventListener('click', packageRepoClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.packageRepo.removeEventListener('click', packageRepoClickHandler) }))
|
||||
|
||||
const issueButtonClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
let bugUri = this.packageManager.getRepositoryBugUri(this.pack)
|
||||
if (bugUri) {
|
||||
shell.openExternal(bugUri)
|
||||
}
|
||||
}
|
||||
this.refs.issueButton.addEventListener('click', issueButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.issueButton.removeEventListener('click', issueButtonClickHandler) }))
|
||||
|
||||
const changelogButtonClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
if (this.changelogPath) {
|
||||
this.openMarkdownFile(this.changelogPath)
|
||||
}
|
||||
}
|
||||
this.refs.changelogButton.addEventListener('click', changelogButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.changelogButton.removeEventListener('click', changelogButtonClickHandler) }))
|
||||
|
||||
const licenseButtonClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
if (this.licensePath) {
|
||||
this.openMarkdownFile(this.licensePath)
|
||||
}
|
||||
}
|
||||
this.refs.licenseButton.addEventListener('click', licenseButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.licenseButton.removeEventListener('click', licenseButtonClickHandler) }))
|
||||
|
||||
const openButtonClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
if (fs.existsSync(this.pack.path)) {
|
||||
atom.open({pathsToOpen: [this.pack.path]})
|
||||
}
|
||||
}
|
||||
this.refs.openButton.addEventListener('click', openButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.openButton.removeEventListener('click', openButtonClickHandler) }))
|
||||
|
||||
const learnMoreButtonClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`)
|
||||
}
|
||||
this.refs.learnMoreButton.addEventListener('click', learnMoreButtonClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.learnMoreButton.removeEventListener('click', learnMoreButtonClickHandler) }))
|
||||
|
||||
const breadcrumbClickHandler = (event) => {
|
||||
event.preventDefault()
|
||||
this.settingsView.showPanel(this.breadcrumbBackPanel)
|
||||
}
|
||||
this.refs.breadcrumb.addEventListener('click', breadcrumbClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.breadcrumb.removeEventListener('click', breadcrumbClickHandler) }))
|
||||
}
|
||||
|
||||
completeInitialization () {
|
||||
if (this.refs.packageCard) {
|
||||
this.packageCard = this.refs.packageCard.packageCard
|
||||
} else if (!this.packageCard) { // Had to load this from the network
|
||||
this.packageCard = new PackageCard(this.pack.metadata, this.settingsView, this.packageManager, {onSettingsView: true})
|
||||
this.refs.packageCardParent.replaceChild(this.packageCard.element, this.refs.loadingMessage)
|
||||
}
|
||||
|
||||
this.refs.packageRepo.classList.remove('hidden')
|
||||
this.refs.startupTime.classList.remove('hidden')
|
||||
this.refs.buttons.classList.remove('hidden')
|
||||
this.activateConfig()
|
||||
this.populate()
|
||||
this.updateFileButtons()
|
||||
this.subscribeToPackageManager()
|
||||
this.renderReadme()
|
||||
}
|
||||
|
||||
loadPackage () {
|
||||
const loadedPackage = atom.packages.getLoadedPackage(this.pack.name)
|
||||
if (loadedPackage) {
|
||||
this.pack = loadedPackage
|
||||
this.completeInitialization()
|
||||
} else {
|
||||
// If the package metadata in `@pack` isn't complete, hit the network.
|
||||
if (!this.pack.metadata || !this.pack.metadata.owner) {
|
||||
this.fetchPackage()
|
||||
} else {
|
||||
this.completeInitialization()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchPackage () {
|
||||
this.showLoadingMessage()
|
||||
this.packageManager.getClient().package(this.pack.name, (err, packageData) => {
|
||||
if (err || !packageData || !packageData.name) {
|
||||
this.hideLoadingMessage()
|
||||
this.showErrorMessage()
|
||||
} else {
|
||||
this.pack = packageData
|
||||
// TODO: this should match Package.loadMetadata from core, but this is
|
||||
// an acceptable hacky workaround
|
||||
this.pack.metadata = _.extend(this.pack.metadata != null ? this.pack.metadata : {}, this.pack)
|
||||
this.completeInitialization()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showLoadingMessage () {
|
||||
this.refs.loadingMessage.classList.remove('hidden')
|
||||
}
|
||||
|
||||
hideLoadingMessage () {
|
||||
this.refs.loadingMessage.classList.add('hidden')
|
||||
}
|
||||
|
||||
showErrorMessage () {
|
||||
this.refs.errorMessage.classList.remove('hidden')
|
||||
}
|
||||
|
||||
hideErrorMessage () {
|
||||
this.refs.errorMessage.classList.add('hidden')
|
||||
}
|
||||
|
||||
activateConfig () {
|
||||
// Package.activateConfig() is part of the Private package API and should not be used outside of core.
|
||||
if (atom.packages.isPackageLoaded(this.pack.name) && !atom.packages.isPackageActive(this.pack.name)) {
|
||||
this.pack.activateConfig()
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (this.settingsPanel) {
|
||||
this.settingsPanel.destroy()
|
||||
this.settingsPanel = null
|
||||
}
|
||||
|
||||
if (this.keymapView) {
|
||||
this.keymapView.destroy()
|
||||
this.keymapView = null
|
||||
}
|
||||
|
||||
if (this.grammarsView) {
|
||||
this.grammarsView.destroy()
|
||||
this.grammarsView = null
|
||||
}
|
||||
|
||||
if (this.snippetsView) {
|
||||
this.snippetsView.destroy()
|
||||
this.snippetsView = null
|
||||
}
|
||||
|
||||
if (this.readmeView) {
|
||||
this.readmeView.destroy()
|
||||
this.readmeView = null
|
||||
}
|
||||
|
||||
if (this.packageCard) {
|
||||
this.packageCard.destroy()
|
||||
this.packageCard = null
|
||||
}
|
||||
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
beforeShow (opts) {
|
||||
if (opts.back == null) {
|
||||
opts.back = 'Install'
|
||||
}
|
||||
|
||||
this.breadcrumbBackPanel = opts.back
|
||||
this.refs.breadcrumb.textContent = this.breadcrumbBackPanel
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
render () {
|
||||
let packageCardView
|
||||
if (this.pack && this.pack.metadata && this.pack.metadata.owner) {
|
||||
packageCardView = (
|
||||
<div ref='packageCardParent' className='row'>
|
||||
<PackageCardComponent
|
||||
ref='packageCard'
|
||||
settingsView={this.settingsView}
|
||||
packageManager={this.packageManager}
|
||||
metadata={this.pack.metadata}
|
||||
options={{onSettingsView: true}} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
packageCardView = (
|
||||
<div ref='packageCardParent' className='row'>
|
||||
<div ref='loadingMessage' className='alert alert-info icon icon-hourglass'>{`Loading ${this.pack.name}\u2026`}</div>
|
||||
<div ref='errorMessage' className='alert alert-danger icon icon-hourglass hidden'>Failed to load {this.pack.name} - try again later.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div tabIndex='0' className='package-detail'>
|
||||
<ol ref='breadcrumbContainer' className='native-key-bindings breadcrumb' tabIndex='-1'>
|
||||
<li>
|
||||
<a ref='breadcrumb' />
|
||||
</li>
|
||||
<li className='active'>
|
||||
<a ref='title' />
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className='panels-item'>
|
||||
<section className='section'>
|
||||
<form className='section-container package-detail-view'>
|
||||
<div className='container package-container'>
|
||||
{packageCardView}
|
||||
</div>
|
||||
|
||||
<p ref='packageRepo' className='link icon icon-repo repo-link hidden' />
|
||||
<p ref='startupTime' className='text icon icon-dashboard hidden' tabIndex='-1' />
|
||||
|
||||
<div ref='buttons' className='btn-wrap-group hidden'>
|
||||
<button ref='learnMoreButton' className='btn btn-default icon icon-link'>View on pulsar-edit.dev</button>
|
||||
<button ref='issueButton' className='btn btn-default icon icon-bug'>Report Issue</button>
|
||||
<button ref='changelogButton' className='btn btn-default icon icon-squirrel'>CHANGELOG</button>
|
||||
<button ref='licenseButton' className='btn btn-default icon icon-law'>LICENSE</button>
|
||||
<button ref='openButton' className='btn btn-default icon icon-link-external'>View Code</button>
|
||||
</div>
|
||||
|
||||
<div ref='errors' />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div ref='sections' />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
populate () {
|
||||
this.refs.title.textContent = `${_.undasherize(_.uncamelcase(this.pack.name))}`
|
||||
this.type = this.pack.metadata.theme ? 'theme' : 'package'
|
||||
|
||||
const repoUrl = this.packageManager.getRepositoryUrl(this.pack)
|
||||
if (repoUrl) {
|
||||
const repoName = url.parse(repoUrl).pathname
|
||||
this.refs.packageRepo.textContent = repoName.substring(1)
|
||||
this.refs.packageRepo.style.display = ''
|
||||
} else {
|
||||
this.refs.packageRepo.style.display = 'none'
|
||||
}
|
||||
|
||||
this.updateInstalledState()
|
||||
}
|
||||
|
||||
updateInstalledState () {
|
||||
if (this.settingsPanel) {
|
||||
this.settingsPanel.destroy()
|
||||
this.settingsPanel = null
|
||||
}
|
||||
|
||||
if (this.keymapView) {
|
||||
this.keymapView.destroy()
|
||||
this.keymapView = null
|
||||
}
|
||||
|
||||
if (this.grammarsView) {
|
||||
this.grammarsView.destroy()
|
||||
this.grammarsView = null
|
||||
}
|
||||
|
||||
if (this.snippetsView) {
|
||||
this.snippetsView.destroy()
|
||||
this.snippetsView = null
|
||||
}
|
||||
|
||||
if (this.readmeView) {
|
||||
this.readmeView.destroy()
|
||||
this.readmeView = null
|
||||
}
|
||||
|
||||
this.updateFileButtons()
|
||||
this.activateConfig()
|
||||
this.refs.startupTime.style.display = 'none'
|
||||
|
||||
if (atom.packages.isPackageLoaded(this.pack.name)) {
|
||||
if (!atom.packages.isPackageDisabled(this.pack.name)) {
|
||||
this.settingsPanel = new SettingsPanel({namespace: this.pack.name, includeTitle: false})
|
||||
this.keymapView = new PackageKeymapView(this.pack)
|
||||
this.refs.sections.appendChild(this.settingsPanel.element)
|
||||
this.refs.sections.appendChild(this.keymapView.element)
|
||||
|
||||
if (this.pack.path) {
|
||||
this.grammarsView = new PackageGrammarsView(this.pack.path)
|
||||
this.snippetsView = new PackageSnippetsView(this.pack, this.snippetsProvider)
|
||||
this.refs.sections.appendChild(this.grammarsView.element)
|
||||
this.refs.sections.appendChild(this.snippetsView.element)
|
||||
}
|
||||
|
||||
this.refs.startupTime.innerHTML =
|
||||
`This ${this.type} added <span class='highlight'>${this.getStartupTime()}ms</span> to startup time.`
|
||||
this.refs.startupTime.style.display = ''
|
||||
}
|
||||
}
|
||||
|
||||
const sourceIsAvailable = this.packageManager.isPackageInstalled(this.pack.name) && !atom.packages.isBundledPackage(this.pack.name)
|
||||
if (sourceIsAvailable) {
|
||||
this.refs.openButton.style.display = ''
|
||||
} else {
|
||||
this.refs.openButton.style.display = 'none'
|
||||
}
|
||||
|
||||
this.renderReadme()
|
||||
}
|
||||
|
||||
renderReadme () {
|
||||
let readme
|
||||
if (this.pack.metadata.readme && this.pack.metadata.readme.trim() !== NORMALIZE_PACKAGE_DATA_README_ERROR) {
|
||||
readme = this.pack.metadata.readme
|
||||
} else {
|
||||
readme = null
|
||||
}
|
||||
|
||||
if (this.readmePath && fs.existsSync(this.readmePath) && fs.statSync(this.readmePath).isFile() && !readme) {
|
||||
readme = fs.readFileSync(this.readmePath, {encoding: 'utf8'})
|
||||
}
|
||||
|
||||
let readmeSrc
|
||||
|
||||
if (this.pack.path) {
|
||||
// If package is installed, use installed path
|
||||
readmeSrc = this.pack.path
|
||||
} else {
|
||||
// If package isn't installed, use url path
|
||||
let repoUrl = this.packageManager.getRepositoryUrl(this.pack)
|
||||
|
||||
// Check if URL is undefined (i.e. package is unpublished)
|
||||
if (repoUrl) {
|
||||
readmeSrc = repoUrl + `/blob/master/`
|
||||
}
|
||||
}
|
||||
|
||||
const readmeView = new PackageReadmeView(readme, readmeSrc)
|
||||
if (this.readmeView) {
|
||||
this.readmeView.element.parentElement.replaceChild(readmeView.element, this.readmeView.element)
|
||||
this.readmeView.destroy()
|
||||
} else {
|
||||
this.refs.sections.appendChild(readmeView.element)
|
||||
}
|
||||
this.readmeView = readmeView
|
||||
}
|
||||
|
||||
subscribeToPackageManager () {
|
||||
this.disposables.add(this.packageManager.on('theme-installed package-installed', ({pack}) => {
|
||||
if (this.pack.name === pack.name) {
|
||||
this.loadPackage()
|
||||
this.updateInstalledState()
|
||||
}
|
||||
}))
|
||||
|
||||
this.disposables.add(this.packageManager.on('theme-uninstalled package-uninstalled', ({pack}) => {
|
||||
if (this.pack.name === pack.name) {
|
||||
return this.updateInstalledState()
|
||||
}
|
||||
}))
|
||||
|
||||
this.disposables.add(this.packageManager.on('theme-updated package-updated', ({pack}) => {
|
||||
if (this.pack.name === pack.name) {
|
||||
this.loadPackage()
|
||||
this.updateFileButtons()
|
||||
this.populate()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
openMarkdownFile (path) {
|
||||
if (atom.packages.isPackageActive('markdown-preview')) {
|
||||
atom.workspace.open(encodeURI(`markdown-preview://${path}`))
|
||||
} else {
|
||||
atom.workspace.open(path)
|
||||
}
|
||||
}
|
||||
|
||||
updateFileButtons () {
|
||||
this.changelogPath = null
|
||||
this.licensePath = null
|
||||
this.readmePath = null
|
||||
|
||||
const packagePath = this.pack.path != null ? this.pack.path : atom.packages.resolvePackagePath(this.pack.name)
|
||||
for (const child of fs.listSync(packagePath)) {
|
||||
switch (path.basename(child, path.extname(child)).toLowerCase()) {
|
||||
case 'changelog':
|
||||
case 'history':
|
||||
this.changelogPath = child
|
||||
break
|
||||
case 'license':
|
||||
case 'licence':
|
||||
this.licensePath = child
|
||||
break
|
||||
case 'readme':
|
||||
this.readmePath = child
|
||||
break
|
||||
}
|
||||
|
||||
if (this.readmePath && this.changelogPath && this.licensePath) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (this.changelogPath) {
|
||||
this.refs.changelogButton.style.display = ''
|
||||
} else {
|
||||
this.refs.changelogButton.style.display = 'none'
|
||||
}
|
||||
|
||||
if (this.licensePath) {
|
||||
this.refs.licenseButton.style.display = ''
|
||||
} else {
|
||||
this.refs.licenseButton.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
getStartupTime () {
|
||||
const loadTime = this.pack.loadTime != null ? this.pack.loadTime : 0
|
||||
const activateTime = this.pack.activateTime != null ? this.pack.activateTime : 0
|
||||
return loadTime + activateTime
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
class PackageCardComponent {
|
||||
constructor (props) {
|
||||
this.packageCard = new PackageCard(props.metadata, props.settingsView, props.packageManager, props.options)
|
||||
this.element = this.packageCard.element
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
destroy () {}
|
||||
}
|
97
packages/settings-view/lib/package-grammars-view.js
Normal file
97
packages/settings-view/lib/package-grammars-view.js
Normal file
@ -0,0 +1,97 @@
|
||||
/** @babel */
|
||||
|
||||
import path from 'path'
|
||||
import {CompositeDisposable} from 'atom'
|
||||
import SettingsPanel from './settings-panel'
|
||||
|
||||
// View to display the grammars that a package has registered.
|
||||
export default class PackageGrammarsView {
|
||||
constructor (packagePath) {
|
||||
this.element = document.createElement('section')
|
||||
this.element.classList.add('package-grammars')
|
||||
|
||||
this.grammarSettings = document.createElement('div')
|
||||
this.element.appendChild(this.grammarSettings)
|
||||
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.packagePath = path.join(packagePath, path.sep)
|
||||
this.addGrammars()
|
||||
this.disposables.add(atom.grammars.onDidAddGrammar(() => this.addGrammars()))
|
||||
this.disposables.add(atom.grammars.onDidUpdateGrammar(() => this.addGrammars()))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
this.element.remove()
|
||||
}
|
||||
|
||||
getPackageGrammars () {
|
||||
const packageGrammars = []
|
||||
const grammars = atom.grammars.grammars != null ? atom.grammars.grammars : []
|
||||
for (let grammar of grammars) {
|
||||
if (grammar.path) {
|
||||
if (grammar.path.indexOf(this.packagePath) === 0) {
|
||||
packageGrammars.push(grammar)
|
||||
}
|
||||
}
|
||||
}
|
||||
return packageGrammars.sort(function (grammar1, grammar2) {
|
||||
const name1 = grammar1.name || grammar1.scopeName || ''
|
||||
const name2 = grammar2.name || grammar2.scopeName || ''
|
||||
return name1.localeCompare(name2)
|
||||
})
|
||||
}
|
||||
|
||||
addGrammarHeading (grammar, panel) {
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('native-key-bindings', 'text')
|
||||
container.tabIndex = -1
|
||||
|
||||
const grammarScope = document.createElement('div')
|
||||
grammarScope.classList.add('grammar-scope')
|
||||
|
||||
const scopeStrong = document.createElement('strong')
|
||||
scopeStrong.textContent = 'Scope: '
|
||||
grammarScope.appendChild(scopeStrong)
|
||||
|
||||
const scopeSpan = document.createElement('span')
|
||||
scopeSpan.textContent = grammar.scopeName != null ? grammar.scopeName : ''
|
||||
grammarScope.appendChild(scopeSpan)
|
||||
container.appendChild(grammarScope)
|
||||
|
||||
const grammarFileTypes = document.createElement('div')
|
||||
grammarFileTypes.classList.add('grammar-filetypes')
|
||||
|
||||
const fileTypesStrong = document.createElement('strong')
|
||||
fileTypesStrong.textContent = 'File Types: '
|
||||
grammarFileTypes.appendChild(fileTypesStrong)
|
||||
|
||||
const fileTypes = grammar.fileTypes || []
|
||||
const fileTypesSpan = document.createElement('span')
|
||||
fileTypesSpan.textContent = fileTypes.join(', ')
|
||||
grammarFileTypes.appendChild(fileTypesSpan)
|
||||
container.appendChild(grammarFileTypes)
|
||||
|
||||
const sectionBody = panel.element.querySelector('.section-body')
|
||||
sectionBody.parentElement.insertBefore(container, sectionBody)
|
||||
}
|
||||
|
||||
addGrammars () {
|
||||
this.grammarSettings.innerHTML = ''
|
||||
for (let grammar of this.getPackageGrammars()) {
|
||||
let {scopeName, name} = grammar
|
||||
if (!scopeName || !name) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!scopeName.startsWith('.')) {
|
||||
scopeName = `.${scopeName}`
|
||||
}
|
||||
|
||||
const title = `${name} Grammar`
|
||||
const panel = new SettingsPanel({title, scopeName, icon: 'puzzle'})
|
||||
this.addGrammarHeading(grammar, panel)
|
||||
this.grammarSettings.appendChild(panel.element)
|
||||
}
|
||||
}
|
||||
}
|
186
packages/settings-view/lib/package-keymap-view.js
Normal file
186
packages/settings-view/lib/package-keymap-view.js
Normal file
@ -0,0 +1,186 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import path from 'path'
|
||||
import _ from 'underscore-plus'
|
||||
import {Disposable, CompositeDisposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
import KeybindingsPanel from './keybindings-panel'
|
||||
|
||||
// Displays the keybindings for a package namespace
|
||||
export default class PackageKeymapView {
|
||||
constructor (pack) {
|
||||
this.pack = pack
|
||||
this.otherPlatformPattern = new RegExp(`\\.platform-(?!${_.escapeRegExp(process.platform)}\\b)`)
|
||||
this.namespace = this.pack.name
|
||||
this.disposables = new CompositeDisposable()
|
||||
etch.initialize(this)
|
||||
|
||||
const packagesWithKeymapsDisabled = atom.config.get('core.packagesWithKeymapsDisabled') || []
|
||||
this.refs.keybindingToggle.checked = !packagesWithKeymapsDisabled.includes(this.namespace)
|
||||
|
||||
const changeHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
const value = this.refs.keybindingToggle.checked
|
||||
if (value) {
|
||||
atom.config.removeAtKeyPath('core.packagesWithKeymapsDisabled', this.namespace)
|
||||
} else {
|
||||
atom.config.pushAtKeyPath('core.packagesWithKeymapsDisabled', this.namespace)
|
||||
}
|
||||
|
||||
this.updateKeyBindingView()
|
||||
}
|
||||
this.refs.keybindingToggle.addEventListener('change', changeHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.keybindingToggle.removeEventListener('change', changeHandler) }))
|
||||
|
||||
const copyIconClickHandler = (event) => {
|
||||
const target = event.target.closest('.copy-icon')
|
||||
if (target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.writeKeyBindingToClipboard(target.closest('tr').dataset)
|
||||
}
|
||||
}
|
||||
this.element.addEventListener('click', copyIconClickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.element.removeEventListener('click', copyIconClickHandler) }))
|
||||
|
||||
this.updateKeyBindingView()
|
||||
|
||||
let hasKeymaps = false
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (let [packageKeymapsPath, keymap] of atom.packages.getLoadedPackage(this.namespace).keymaps) {
|
||||
if (keymap.length > 0) {
|
||||
hasKeymaps = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (this.refs.keybindingItems.children.length === 0 && !hasKeymaps) {
|
||||
this.element.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<section className='section'>
|
||||
<div className='section-heading icon icon-keyboard'>Keybindings</div>
|
||||
<div className='checkbox'>
|
||||
<label for='toggleKeybindings'>
|
||||
<input id='toggleKeybindings' className='input-checkbox' type='checkbox' ref='keybindingToggle' />
|
||||
<div className='setting-title'>Enable</div>
|
||||
</label>
|
||||
<div className='setting-description'>
|
||||
{"Disable this if you want to bind your own keystrokes for this package's commands in your keymap."}
|
||||
</div>
|
||||
</div>
|
||||
<table className='package-keymap-table table native-key-bindings text' tabIndex='-1'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Keystroke</th>
|
||||
<th>Command</th>
|
||||
<th>Selector</th>
|
||||
<th>Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ref='keybindingItems' />
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
updateKeyBindingView () {
|
||||
this.refs.keybindingItems.innerHTML = ''
|
||||
|
||||
const packagesWithKeymapsDisabled = atom.config.get('core.packagesWithKeymapsDisabled') || []
|
||||
const keybindingsDisabled = packagesWithKeymapsDisabled.includes(this.namespace)
|
||||
if (keybindingsDisabled) {
|
||||
this.refs.keybindingItems.classList.add('text-subtle')
|
||||
} else {
|
||||
this.refs.keybindingItems.classList.remove('text-subtle')
|
||||
}
|
||||
|
||||
const keyBindings = []
|
||||
if (atom.keymaps.build) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const [keymapPath, keymap] of atom.packages.getLoadedPackage(this.namespace).keymaps) {
|
||||
keyBindings.push(...atom.keymaps.build(this.namespace, keymap, 0, false))
|
||||
}
|
||||
} else {
|
||||
// Backwards compatibility for Atom <= 1.19
|
||||
for (const keyBinding of atom.keymaps.getKeyBindings()) {
|
||||
const {command} = keyBinding
|
||||
if (command && command.indexOf && command.indexOf(`${this.namespace}:`) === 0) {
|
||||
keyBindings.push(keyBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const keyBinding of keyBindings) {
|
||||
const {command, keystrokes, selector, source} = keyBinding
|
||||
if (!command) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (this.otherPlatformPattern.test(selector)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const keyBindingRow = document.createElement('tr')
|
||||
keyBindingRow.dataset.selector = selector
|
||||
keyBindingRow.dataset.keystrokes = keystrokes
|
||||
keyBindingRow.dataset.command = command
|
||||
|
||||
const keystrokesTd = document.createElement('td')
|
||||
|
||||
const copyIconSpan = document.createElement('span')
|
||||
copyIconSpan.classList.add('icon', 'icon-clippy', 'copy-icon')
|
||||
keystrokesTd.appendChild(copyIconSpan)
|
||||
|
||||
const keystrokesSpan = document.createElement('span')
|
||||
keystrokesSpan.textContent = keystrokes
|
||||
keystrokesTd.appendChild(keystrokesSpan)
|
||||
|
||||
keyBindingRow.appendChild(keystrokesTd)
|
||||
|
||||
const commandTd = document.createElement('td')
|
||||
commandTd.textContent = command
|
||||
keyBindingRow.appendChild(commandTd)
|
||||
|
||||
const selectorTd = document.createElement('td')
|
||||
selectorTd.textContent = selector
|
||||
keyBindingRow.appendChild(selectorTd)
|
||||
|
||||
const sourceTd = document.createElement('td')
|
||||
sourceTd.textContent = KeybindingsPanel.determineSource(source)
|
||||
keyBindingRow.appendChild(sourceTd)
|
||||
|
||||
this.refs.keybindingItems.appendChild(keyBindingRow)
|
||||
}
|
||||
}
|
||||
|
||||
writeKeyBindingToClipboard ({selector, keystrokes, command}) {
|
||||
let content
|
||||
const keymapExtension = path.extname(atom.keymaps.getUserKeymapPath())
|
||||
if (keymapExtension === '.cson') {
|
||||
content = `\
|
||||
'${selector}':
|
||||
'${keystrokes}': '${command}'\
|
||||
`
|
||||
} else {
|
||||
content = `\
|
||||
"${selector}": {
|
||||
"${keystrokes}": "${command}"
|
||||
}\
|
||||
`
|
||||
}
|
||||
|
||||
atom.clipboard.write(content)
|
||||
}
|
||||
}
|
445
packages/settings-view/lib/package-manager.coffee
Normal file
445
packages/settings-view/lib/package-manager.coffee
Normal file
@ -0,0 +1,445 @@
|
||||
_ = require 'underscore-plus'
|
||||
{BufferedProcess, CompositeDisposable, Emitter} = require 'atom'
|
||||
semver = require 'semver'
|
||||
|
||||
Client = require './atom-io-client'
|
||||
|
||||
module.exports =
|
||||
class PackageManager
|
||||
# Millisecond expiry for cached loadOutdated, etc. values
|
||||
CACHE_EXPIRY: 1000*60*10
|
||||
|
||||
constructor: ->
|
||||
@packagePromises = []
|
||||
@apmCache =
|
||||
loadOutdated:
|
||||
value: null
|
||||
expiry: 0
|
||||
|
||||
@emitter = new Emitter
|
||||
|
||||
getClient: ->
|
||||
@client ?= new Client(this)
|
||||
|
||||
isPackageInstalled: (packageName) ->
|
||||
if atom.packages.isPackageLoaded(packageName)
|
||||
true
|
||||
else
|
||||
atom.packages.getAvailablePackageNames().indexOf(packageName) > -1
|
||||
|
||||
packageHasSettings: (packageName) ->
|
||||
grammars = atom.grammars.getGrammars() ? []
|
||||
for grammar in grammars when grammar.path
|
||||
return true if grammar.packageName is packageName
|
||||
|
||||
pack = atom.packages.getLoadedPackage(packageName)
|
||||
pack.activateConfig() if pack? and not atom.packages.isPackageActive(packageName)
|
||||
schema = atom.config.getSchema(packageName)
|
||||
schema? and (schema.type isnt 'any')
|
||||
|
||||
setProxyServers: (callback) =>
|
||||
session = atom.getCurrentWindow().webContents.session
|
||||
session.resolveProxy 'http://atom.io', (httpProxy) =>
|
||||
@applyProxyToEnv('http_proxy', httpProxy)
|
||||
session.resolveProxy 'https://pulsar-edit.dev', (httpsProxy) =>
|
||||
@applyProxyToEnv('https_proxy', httpsProxy)
|
||||
callback()
|
||||
|
||||
setProxyServersAsync: (callback) =>
|
||||
httpProxyPromise = atom.resolveProxy('http://atom.io').then((proxy) => @applyProxyToEnv('http_proxy', proxy))
|
||||
httpsProxyPromise = atom.resolveProxy('https://pulsar-edit.dev').then((proxy) => @applyProxyToEnv('https_proxy', proxy))
|
||||
Promise.all([httpProxyPromise, httpsProxyPromise]).then(callback)
|
||||
|
||||
applyProxyToEnv: (envName, proxy) ->
|
||||
if proxy?
|
||||
proxy = proxy.split(' ')
|
||||
switch proxy[0].trim().toUpperCase()
|
||||
when 'DIRECT' then delete process.env[envName]
|
||||
when 'PROXY' then process.env[envName] = 'http://' + proxy[1]
|
||||
return
|
||||
|
||||
runCommand: (args, callback) ->
|
||||
command = atom.packages.getApmPath()
|
||||
outputLines = []
|
||||
stdout = (lines) -> outputLines.push(lines)
|
||||
errorLines = []
|
||||
stderr = (lines) -> errorLines.push(lines)
|
||||
exit = (code) ->
|
||||
callback(code, outputLines.join('\n'), errorLines.join('\n'))
|
||||
|
||||
args.push('--no-color')
|
||||
|
||||
if atom.config.get('core.useProxySettingsWhenCallingApm')
|
||||
bufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit, autoStart: false})
|
||||
if atom.resolveProxy?
|
||||
@setProxyServersAsync -> bufferedProcess.start()
|
||||
else
|
||||
@setProxyServers -> bufferedProcess.start()
|
||||
return bufferedProcess
|
||||
else
|
||||
return new BufferedProcess({command, args, stdout, stderr, exit})
|
||||
|
||||
loadInstalled: (callback) ->
|
||||
args = ['ls', '--json']
|
||||
errorMessage = 'Fetching local packages failed.'
|
||||
apmProcess = @runCommand args, (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
try
|
||||
packages = JSON.parse(stdout) ? []
|
||||
catch parseError
|
||||
error = createJsonParseError(errorMessage, parseError, stdout)
|
||||
return callback(error)
|
||||
callback(null, packages)
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
callback(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadFeatured: (loadThemes, callback) ->
|
||||
unless callback
|
||||
callback = loadThemes
|
||||
loadThemes = false
|
||||
|
||||
args = ['featured', '--json']
|
||||
version = atom.getVersion()
|
||||
args.push('--themes') if loadThemes
|
||||
args.push('--compatible', version) if semver.valid(version)
|
||||
errorMessage = 'Fetching featured packages failed.'
|
||||
|
||||
apmProcess = @runCommand args, (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
try
|
||||
packages = JSON.parse(stdout) ? []
|
||||
catch parseError
|
||||
error = createJsonParseError(errorMessage, parseError, stdout)
|
||||
return callback(error)
|
||||
|
||||
callback(null, packages)
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
callback(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadOutdated: (clearCache, callback) ->
|
||||
if clearCache
|
||||
@clearOutdatedCache()
|
||||
# Short circuit if we have cached data.
|
||||
else if @apmCache.loadOutdated.value and @apmCache.loadOutdated.expiry > Date.now()
|
||||
return callback(null, @apmCache.loadOutdated.value)
|
||||
|
||||
args = ['outdated', '--json']
|
||||
version = atom.getVersion()
|
||||
args.push('--compatible', version) if semver.valid(version)
|
||||
errorMessage = 'Fetching outdated packages and themes failed.'
|
||||
|
||||
apmProcess = @runCommand args, (code, stdout, stderr) =>
|
||||
if code is 0
|
||||
try
|
||||
packages = JSON.parse(stdout) ? []
|
||||
catch parseError
|
||||
error = createJsonParseError(errorMessage, parseError, stdout)
|
||||
return callback(error)
|
||||
|
||||
updatablePackages = (pack for pack in packages when not @getVersionPinnedPackages().includes(pack?.name))
|
||||
|
||||
@apmCache.loadOutdated =
|
||||
value: updatablePackages
|
||||
expiry: Date.now() + @CACHE_EXPIRY
|
||||
|
||||
for pack in updatablePackages
|
||||
@emitPackageEvent 'update-available', pack
|
||||
|
||||
callback(null, updatablePackages)
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
callback(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
getVersionPinnedPackages: ->
|
||||
atom.config.get('core.versionPinnedPackages') ? []
|
||||
|
||||
clearOutdatedCache: ->
|
||||
@apmCache.loadOutdated =
|
||||
value: null
|
||||
expiry: 0
|
||||
|
||||
loadPackage: (packageName, callback) ->
|
||||
args = ['view', packageName, '--json']
|
||||
errorMessage = "Fetching package '#{packageName}' failed."
|
||||
|
||||
apmProcess = @runCommand args, (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
try
|
||||
packages = JSON.parse(stdout) ? []
|
||||
catch parseError
|
||||
error = createJsonParseError(errorMessage, parseError, stdout)
|
||||
return callback(error)
|
||||
|
||||
callback(null, packages)
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
callback(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
loadCompatiblePackageVersion: (packageName, callback) ->
|
||||
args = ['view', packageName, '--json', '--compatible', @normalizeVersion(atom.getVersion())]
|
||||
errorMessage = "Fetching package '#{packageName}' failed."
|
||||
|
||||
apmProcess = @runCommand args, (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
try
|
||||
packages = JSON.parse(stdout) ? []
|
||||
catch parseError
|
||||
error = createJsonParseError(errorMessage, parseError, stdout)
|
||||
return callback(error)
|
||||
|
||||
callback(null, packages)
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
callback(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, callback)
|
||||
|
||||
getInstalled: ->
|
||||
new Promise (resolve, reject) =>
|
||||
@loadInstalled (error, result) ->
|
||||
if error
|
||||
reject(error)
|
||||
else
|
||||
resolve(result)
|
||||
|
||||
getFeatured: (loadThemes) ->
|
||||
new Promise (resolve, reject) =>
|
||||
@loadFeatured !!loadThemes, (error, result) ->
|
||||
if error
|
||||
reject(error)
|
||||
else
|
||||
resolve(result)
|
||||
|
||||
getOutdated: (clearCache = false) ->
|
||||
new Promise (resolve, reject) =>
|
||||
@loadOutdated clearCache, (error, result) ->
|
||||
if error
|
||||
reject(error)
|
||||
else
|
||||
resolve(result)
|
||||
|
||||
getPackage: (packageName) ->
|
||||
@packagePromises[packageName] ?= new Promise (resolve, reject) =>
|
||||
@loadPackage packageName, (error, result) ->
|
||||
if error
|
||||
reject(error)
|
||||
else
|
||||
resolve(result)
|
||||
|
||||
satisfiesVersion: (version, metadata) ->
|
||||
engine = metadata.engines?.atom ? '*'
|
||||
return false unless semver.validRange(engine)
|
||||
return semver.satisfies(version, engine)
|
||||
|
||||
normalizeVersion: (version) ->
|
||||
[version] = version.split('-') if typeof version is 'string'
|
||||
version
|
||||
|
||||
update: (pack, newVersion, callback) ->
|
||||
{name, theme, apmInstallSource} = pack
|
||||
|
||||
errorMessage = if newVersion
|
||||
"Updating to \u201C#{name}@#{newVersion}\u201D failed."
|
||||
else
|
||||
"Updating to latest sha failed."
|
||||
onError = (error) =>
|
||||
error.packageInstallError = not theme
|
||||
@emitPackageEvent 'update-failed', pack, error
|
||||
callback?(error)
|
||||
|
||||
if apmInstallSource?.type is 'git'
|
||||
args = ['install', apmInstallSource.source]
|
||||
else
|
||||
args = ['install', "#{name}@#{newVersion}"]
|
||||
|
||||
exit = (code, stdout, stderr) =>
|
||||
if code is 0
|
||||
@clearOutdatedCache()
|
||||
callback?()
|
||||
@emitPackageEvent 'updated', pack
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
onError(error)
|
||||
|
||||
@emitPackageEvent 'updating', pack
|
||||
apmProcess = @runCommand(args, exit)
|
||||
handleProcessErrors(apmProcess, errorMessage, onError)
|
||||
|
||||
unload: (name) ->
|
||||
if atom.packages.isPackageLoaded(name)
|
||||
atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)
|
||||
atom.packages.unloadPackage(name)
|
||||
|
||||
install: (pack, callback) ->
|
||||
{name, version, theme} = pack
|
||||
activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name)
|
||||
activateOnFailure = atom.packages.isPackageActive(name)
|
||||
nameWithVersion = if version? then "#{name}@#{version}" else name
|
||||
|
||||
@unload(name)
|
||||
args = ['install', nameWithVersion, '--json']
|
||||
|
||||
errorMessage = "Installing \u201C#{nameWithVersion}\u201D failed."
|
||||
onError = (error) =>
|
||||
error.packageInstallError = not theme
|
||||
@emitPackageEvent 'install-failed', pack, error
|
||||
callback?(error)
|
||||
|
||||
exit = (code, stdout, stderr) =>
|
||||
if code is 0
|
||||
# get real package name from package.json
|
||||
try
|
||||
packageInfo = JSON.parse(stdout)[0]
|
||||
pack = _.extend({}, pack, packageInfo.metadata)
|
||||
name = pack.name
|
||||
catch err
|
||||
# using old apm without --json support
|
||||
@clearOutdatedCache()
|
||||
if activateOnSuccess
|
||||
atom.packages.activatePackage(name)
|
||||
else
|
||||
atom.packages.loadPackage(name)
|
||||
|
||||
callback?()
|
||||
@emitPackageEvent 'installed', pack
|
||||
else
|
||||
atom.packages.activatePackage(name) if activateOnFailure
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
onError(error)
|
||||
|
||||
@emitPackageEvent('installing', pack)
|
||||
apmProcess = @runCommand(args, exit)
|
||||
handleProcessErrors(apmProcess, errorMessage, onError)
|
||||
|
||||
uninstall: (pack, callback) ->
|
||||
{name} = pack
|
||||
|
||||
atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)
|
||||
|
||||
errorMessage = "Uninstalling \u201C#{name}\u201D failed."
|
||||
onError = (error) =>
|
||||
@emitPackageEvent 'uninstall-failed', pack, error
|
||||
callback?(error)
|
||||
|
||||
@emitPackageEvent('uninstalling', pack)
|
||||
apmProcess = @runCommand ['uninstall', '--hard', name], (code, stdout, stderr) =>
|
||||
if code is 0
|
||||
@clearOutdatedCache()
|
||||
@unload(name)
|
||||
@removePackageNameFromDisabledPackages(name)
|
||||
callback?()
|
||||
@emitPackageEvent 'uninstalled', pack
|
||||
else
|
||||
error = new Error(errorMessage)
|
||||
error.stdout = stdout
|
||||
error.stderr = stderr
|
||||
onError(error)
|
||||
|
||||
handleProcessErrors(apmProcess, errorMessage, onError)
|
||||
|
||||
canUpgrade: (installedPackage, availableVersion) ->
|
||||
return false unless installedPackage?
|
||||
|
||||
installedVersion = installedPackage.metadata.version
|
||||
return false unless semver.valid(installedVersion)
|
||||
return false unless semver.valid(availableVersion)
|
||||
|
||||
semver.gt(availableVersion, installedVersion)
|
||||
|
||||
getPackageTitle: ({name}) ->
|
||||
_.undasherize(_.uncamelcase(name))
|
||||
|
||||
getRepositoryUrl: ({metadata}) ->
|
||||
{repository} = metadata
|
||||
repoUrl = repository?.url ? repository ? ''
|
||||
if repoUrl.match 'git@github'
|
||||
repoName = repoUrl.split(':')[1]
|
||||
repoUrl = "https://github.com/#{repoName}"
|
||||
repoUrl.replace(/\.git$/, '').replace(/\/+$/, '').replace(/^git\+/, '')
|
||||
|
||||
getRepositoryBugUri: ({metadata}) ->
|
||||
{bugs} = metadata
|
||||
if typeof bugs is 'string'
|
||||
bugUri = bugs
|
||||
else
|
||||
bugUri = bugs?.url ? bugs?.email ? this.getRepositoryUrl({metadata}) + '/issues/new'
|
||||
if bugUri.includes('@')
|
||||
bugUri = 'mailto:' + bugUri
|
||||
bugUri
|
||||
|
||||
checkNativeBuildTools: ->
|
||||
new Promise (resolve, reject) =>
|
||||
apmProcess = @runCommand ['install', '--check'], (code, stdout, stderr) ->
|
||||
if code is 0
|
||||
resolve()
|
||||
else
|
||||
reject(new Error())
|
||||
|
||||
apmProcess.onWillThrowError ({error, handle}) ->
|
||||
handle()
|
||||
reject(error)
|
||||
|
||||
removePackageNameFromDisabledPackages: (packageName) ->
|
||||
atom.config.removeAtKeyPath('core.disabledPackages', packageName)
|
||||
|
||||
# Emits the appropriate event for the given package.
|
||||
#
|
||||
# All events are either of the form `theme-foo` or `package-foo` depending on
|
||||
# whether the event is for a theme or a normal package. This method standardizes
|
||||
# the logic to determine if a package is a theme or not and formats the event
|
||||
# name appropriately.
|
||||
#
|
||||
# eventName - The event name suffix {String} of the event to emit.
|
||||
# pack - The package for which the event is being emitted.
|
||||
# error - Any error information to be included in the case of an error.
|
||||
emitPackageEvent: (eventName, pack, error) ->
|
||||
theme = pack.theme ? pack.metadata?.theme
|
||||
eventName = if theme then "theme-#{eventName}" else "package-#{eventName}"
|
||||
@emitter.emit(eventName, {pack, error})
|
||||
|
||||
on: (selectors, callback) ->
|
||||
subscriptions = new CompositeDisposable
|
||||
for selector in selectors.split(" ")
|
||||
subscriptions.add @emitter.on(selector, callback)
|
||||
subscriptions
|
||||
|
||||
createJsonParseError = (message, parseError, stdout) ->
|
||||
error = new Error(message)
|
||||
error.stdout = ''
|
||||
error.stderr = "#{parseError.message}: #{stdout}"
|
||||
error
|
||||
|
||||
createProcessError = (message, processError) ->
|
||||
error = new Error(message)
|
||||
error.stdout = ''
|
||||
error.stderr = processError.message
|
||||
error
|
||||
|
||||
handleProcessErrors = (apmProcess, message, callback) ->
|
||||
apmProcess.onWillThrowError ({error, handle}) ->
|
||||
handle()
|
||||
callback(createProcessError(message, error))
|
84
packages/settings-view/lib/package-readme-view.js
Normal file
84
packages/settings-view/lib/package-readme-view.js
Normal file
@ -0,0 +1,84 @@
|
||||
/** @babel */
|
||||
|
||||
import {marked} from 'marked'
|
||||
import createDOMPurify from 'dompurify'
|
||||
|
||||
function sanitize (html, readmeSrc) {
|
||||
const temporaryContainer = document.createElement('div')
|
||||
temporaryContainer.innerHTML = html
|
||||
|
||||
for (const checkbox of temporaryContainer.querySelectorAll('input[type="checkbox"]')) {
|
||||
checkbox.setAttribute('disabled', '')
|
||||
}
|
||||
|
||||
let path = require('path')
|
||||
|
||||
for (const image of temporaryContainer.querySelectorAll('img')) {
|
||||
let imageSrc = image.getAttribute('src')
|
||||
|
||||
let changeImageSrc = true
|
||||
|
||||
// If src contains a protocol then it must be absolute
|
||||
if (/^(?:[a-z]+:)?\/\//i.test(imageSrc)) {
|
||||
changeImageSrc = false
|
||||
}
|
||||
|
||||
// If src contains a base64 encoded image it must be left unchanged
|
||||
if (/^data:image\/.*;base64/i.test(imageSrc)) {
|
||||
changeImageSrc = false
|
||||
}
|
||||
|
||||
// If path is absolute on file system it must be a local file, e.g. emoji
|
||||
if (path.isAbsolute(imageSrc)) {
|
||||
changeImageSrc = false
|
||||
}
|
||||
|
||||
// If imageSrc needs changing and readmeSrc isn't undefined (i.e. if package was unpublished)
|
||||
if (changeImageSrc && readmeSrc) {
|
||||
if (path.isAbsolute(readmeSrc)) {
|
||||
// If repoUrl is a local path (i.e. package is installed)
|
||||
image.setAttribute('src', path.join(readmeSrc, imageSrc))
|
||||
} else {
|
||||
// If repoUrl is a URL (i.e. package isn't installed)
|
||||
image.setAttribute('src', new URL(imageSrc, readmeSrc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createDOMPurify().sanitize(temporaryContainer.innerHTML)
|
||||
}
|
||||
|
||||
// Displays the readme for a package, if it has one
|
||||
// TODO Decide to keep this or current button-to-new-tab view
|
||||
export default class PackageReadmeView {
|
||||
constructor (readme, readmeSrc) {
|
||||
this.element = document.createElement('section')
|
||||
this.element.classList.add('section')
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('section-container')
|
||||
|
||||
const heading = document.createElement('div')
|
||||
heading.classList.add('section-heading', 'icon', 'icon-book')
|
||||
heading.textContent = 'README'
|
||||
container.appendChild(heading)
|
||||
|
||||
this.packageReadme = document.createElement('div')
|
||||
this.packageReadme.classList.add('package-readme', 'native-key-bindings')
|
||||
this.packageReadme.tabIndex = -1
|
||||
container.appendChild(this.packageReadme)
|
||||
this.element.appendChild(container)
|
||||
|
||||
marked(readme || '### No README.', {breaks: false}, (err, content) => {
|
||||
if (err) {
|
||||
this.packageReadme.innerHTML = '<h3>Error parsing README</h3>'
|
||||
} else {
|
||||
this.packageReadme.innerHTML = sanitize(content, readmeSrc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.element.remove()
|
||||
}
|
||||
}
|
236
packages/settings-view/lib/package-snippets-view.js
Normal file
236
packages/settings-view/lib/package-snippets-view.js
Normal file
@ -0,0 +1,236 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import path from 'path'
|
||||
import _ from 'underscore-plus'
|
||||
import etch from 'etch'
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
|
||||
// View to display the snippets that a package has registered.
|
||||
export default class PackageSnippetsView {
|
||||
constructor (pack, snippetsProvider) {
|
||||
this.pack = pack
|
||||
this.namespace = this.pack.name
|
||||
this.snippetsProvider = snippetsProvider
|
||||
this.packagePath = path.join(pack.path, path.sep)
|
||||
etch.initialize(this)
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.updateSnippetsView()
|
||||
|
||||
const packagesWithSnippetsDisabled = atom.config.get('core.packagesWithSnippetsDisabled') || []
|
||||
this.refs.snippetToggle.checked = !packagesWithSnippetsDisabled.includes(this.namespace)
|
||||
|
||||
const changeHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
const value = this.refs.snippetToggle.checked
|
||||
if (value) {
|
||||
atom.config.removeAtKeyPath('core.packagesWithSnippetsDisabled', this.namespace)
|
||||
} else {
|
||||
atom.config.pushAtKeyPath('core.packagesWithSnippetsDisabled', this.namespace)
|
||||
}
|
||||
this.updateSnippetsView()
|
||||
}
|
||||
|
||||
this.refs.snippetToggle.addEventListener('change', changeHandler)
|
||||
this.disposables.add(new Disposable(() => { this.refs.snippetToggle.removeEventListener('change', changeHandler) }))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<section className='section'>
|
||||
<div className='section-heading icon icon-code'>Snippets</div>
|
||||
<div className='checkbox'>
|
||||
<label for='toggleSnippets'>
|
||||
<input id='toggleSnippets' className='input-checkbox' type='checkbox' ref='snippetToggle' />
|
||||
<div className='setting-title'>Enable</div>
|
||||
</label>
|
||||
<div className='setting-description'>
|
||||
{'Disable this if you want to prevent this package’s snippets from appearing as suggestions or if you want to customize them in your snippets file.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className='package-snippets-table table native-key-bindings text' tabIndex={-1}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trigger</th>
|
||||
<th>Name</th>
|
||||
<th>Scope</th>
|
||||
<th>Body</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ref='snippets' />
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
getSnippetProperties () {
|
||||
const packageProperties = {}
|
||||
for (const {name, properties, selectorString} of this.snippetsProvider.getSnippets()) {
|
||||
if (name && name.indexOf && name.indexOf(this.packagePath) === 0) {
|
||||
const object = properties.snippets != null ? properties.snippets : {}
|
||||
for (let key in object) {
|
||||
const snippet = object[key]
|
||||
if (snippet != null) {
|
||||
snippet.selectorString = selectorString
|
||||
if (packageProperties[key] == null) {
|
||||
packageProperties[key] = snippet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.values(packageProperties).sort((snippet1, snippet2) => {
|
||||
const prefix1 = snippet1.prefix != null ? snippet1.prefix : ''
|
||||
const prefix2 = snippet2.prefix != null ? snippet2.prefix : ''
|
||||
return prefix1.localeCompare(prefix2)
|
||||
})
|
||||
}
|
||||
|
||||
getSnippets (callback) {
|
||||
const snippetsPackage = atom.packages.getLoadedPackage('snippets')
|
||||
const snippetsModule = snippetsPackage ? snippetsPackage.mainModule : null
|
||||
if (snippetsModule) {
|
||||
if (snippetsModule.loaded) {
|
||||
callback(this.getSnippetProperties())
|
||||
} else {
|
||||
snippetsModule.onDidLoadSnippets(() => callback(this.getSnippetProperties()))
|
||||
}
|
||||
} else {
|
||||
callback([]) // eslint-disable-line standard/no-callback-literal
|
||||
}
|
||||
}
|
||||
|
||||
updateSnippetsView () {
|
||||
const packagesWithSnippetsDisabled = atom.config.get('core.packagesWithSnippetsDisabled') || []
|
||||
const snippetsDisabled = packagesWithSnippetsDisabled.includes(this.namespace)
|
||||
|
||||
this.getSnippets((snippets) => {
|
||||
this.refs.snippets.innerHTML = ''
|
||||
|
||||
if (snippetsDisabled) {
|
||||
this.refs.snippets.classList.add('text-subtle')
|
||||
} else {
|
||||
this.refs.snippets.classList.remove('text-subtle')
|
||||
}
|
||||
|
||||
for (let {body, bodyText, name, prefix, selectorString} of snippets) {
|
||||
if (name == null) {
|
||||
name = ''
|
||||
}
|
||||
|
||||
if (prefix == null) {
|
||||
prefix = ''
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
body = bodyText || ''
|
||||
}
|
||||
|
||||
if (selectorString == null) {
|
||||
selectorString = ''
|
||||
}
|
||||
|
||||
const row = document.createElement('tr')
|
||||
|
||||
const prefixTd = document.createElement('td')
|
||||
prefixTd.classList.add('snippet-prefix')
|
||||
prefixTd.textContent = prefix
|
||||
row.appendChild(prefixTd)
|
||||
|
||||
const nameTd = document.createElement('td')
|
||||
nameTd.textContent = name
|
||||
row.appendChild(nameTd)
|
||||
|
||||
const scopeTd = document.createElement('td')
|
||||
scopeTd.classList.add('snippet-scope-name')
|
||||
scopeTd.textContent = selectorString
|
||||
row.appendChild(scopeTd)
|
||||
|
||||
const bodyTd = document.createElement('td')
|
||||
bodyTd.classList.add('snippet-body')
|
||||
row.appendChild(bodyTd)
|
||||
|
||||
this.refs.snippets.appendChild(row)
|
||||
this.createButtonsForSnippetRow(bodyTd, {body, prefix, scope: selectorString, name})
|
||||
}
|
||||
|
||||
if (this.refs.snippets.children.length > 0) {
|
||||
this.element.style.display = ''
|
||||
} else {
|
||||
this.element.style.display = 'none'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createButtonsForSnippetRow (td, {scope, body, name, prefix}) {
|
||||
let buttonContainer = document.createElement('div')
|
||||
buttonContainer.classList.add('btn-group', 'btn-group-xs')
|
||||
|
||||
let viewButton = document.createElement('button')
|
||||
let copyButton = document.createElement('button')
|
||||
|
||||
viewButton.setAttribute('type', 'button')
|
||||
viewButton.textContent = 'View'
|
||||
viewButton.classList.add('btn', 'snippet-view-btn')
|
||||
|
||||
let tooltip = atom.tooltips.add(viewButton, {
|
||||
title: body,
|
||||
html: false,
|
||||
trigger: 'click',
|
||||
placement: 'auto left',
|
||||
'class': 'snippet-body-tooltip'
|
||||
})
|
||||
|
||||
this.disposables.add(tooltip)
|
||||
|
||||
copyButton.setAttribute('type', 'button')
|
||||
copyButton.textContent = 'Copy'
|
||||
copyButton.classList.add('btn', 'snippet-copy-btn')
|
||||
|
||||
copyButton.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
return this.writeSnippetToClipboard({scope, body, name, prefix})
|
||||
})
|
||||
|
||||
buttonContainer.appendChild(viewButton)
|
||||
buttonContainer.appendChild(copyButton)
|
||||
|
||||
td.appendChild(buttonContainer)
|
||||
}
|
||||
|
||||
writeSnippetToClipboard ({scope, body, name, prefix}) {
|
||||
let content
|
||||
const extension = path.extname(this.snippetsProvider.getUserSnippetsPath())
|
||||
body = body.replace(/\n/g, '\\n').replace(/\t/g, '\\t')
|
||||
if (extension === '.cson') {
|
||||
body = body.replace(/'/g, `\\'`)
|
||||
content = `
|
||||
'${scope}':
|
||||
'${name}':
|
||||
'prefix': '${prefix}'
|
||||
'body': '${body}'
|
||||
`
|
||||
} else {
|
||||
body = body.replace(/"/g, `\\"`)
|
||||
content = `
|
||||
"${scope}": {
|
||||
"${name}": {
|
||||
"prefix": "${prefix}",
|
||||
"body": "${body}"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
atom.clipboard.write(content)
|
||||
}
|
||||
}
|
156
packages/settings-view/lib/package-updates-status-view.js
Normal file
156
packages/settings-view/lib/package-updates-status-view.js
Normal file
@ -0,0 +1,156 @@
|
||||
/** @babel */
|
||||
|
||||
import _ from 'underscore-plus'
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
|
||||
export default class PackageUpdatesStatusView {
|
||||
initialize (statusBar, packageManager, updates) {
|
||||
this.statusBar = statusBar
|
||||
this.updates = updates
|
||||
this.destroyed = true
|
||||
this.updatingPackages = []
|
||||
this.failedUpdates = []
|
||||
this.disposables = new CompositeDisposable()
|
||||
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('package-updates-status-view', 'inline-block', 'text', 'text-info')
|
||||
|
||||
const iconPackage = document.createElement('span')
|
||||
iconPackage.classList.add('icon', 'icon-package')
|
||||
this.element.appendChild(iconPackage)
|
||||
|
||||
this.countLabel = document.createElement('span')
|
||||
this.countLabel.classList.add('available-updates-status')
|
||||
this.element.appendChild(this.countLabel)
|
||||
|
||||
this.disposables.add(packageManager.on('package-update-available theme-update-available', ({pack, error}) => { this.onPackageUpdateAvailable(pack) }))
|
||||
this.disposables.add(packageManager.on('package-updating theme-updating', ({pack, error}) => { this.onPackageUpdating(pack) }))
|
||||
this.disposables.add(packageManager.on('package-updated theme-updated package-uninstalled theme-uninstalled', ({pack, error}) => { this.onPackageUpdated(pack) }))
|
||||
this.disposables.add(packageManager.on('package-update-failed theme-update-failed', ({pack, error}) => { this.onPackageUpdateFailed(pack) }))
|
||||
|
||||
const clickHandler = () => {
|
||||
atom.commands.dispatch(atom.views.getView(atom.workspace), 'settings-view:check-for-package-updates')
|
||||
}
|
||||
this.element.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.element.removeEventListener('click', clickHandler) }))
|
||||
|
||||
this.updateTile()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
this.element.remove()
|
||||
|
||||
if (this.tile) {
|
||||
this.tile.destroy()
|
||||
this.tile = null
|
||||
}
|
||||
|
||||
if (this.tooltip) {
|
||||
this.tooltip.dispose()
|
||||
this.tooltip = null
|
||||
}
|
||||
}
|
||||
|
||||
onPackageUpdateAvailable (pack) {
|
||||
for (const update of this.updates) {
|
||||
if (update.name === pack.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.updates.push(pack)
|
||||
this.updateTile()
|
||||
}
|
||||
|
||||
onPackageUpdating (pack) {
|
||||
// Wipe failed status when an update is retried
|
||||
for (let index = 0; index < this.failedUpdates.length; index++) {
|
||||
const update = this.failedUpdates[index]
|
||||
if (update.name === pack.name) {
|
||||
this.failedUpdates.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.updatingPackages.push(pack)
|
||||
this.updateTile()
|
||||
}
|
||||
|
||||
onPackageUpdated (pack) {
|
||||
for (let index = 0; index < this.updates.length; index++) {
|
||||
const update = this.updates[index]
|
||||
if (update.name === pack.name) {
|
||||
this.updates.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.updatingPackages.length; index++) {
|
||||
const update = this.updatingPackages[index]
|
||||
if (update.name === pack.name) {
|
||||
this.updatingPackages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.failedUpdates.length; index++) {
|
||||
const update = this.failedUpdates[index]
|
||||
if (update.name === pack.name) {
|
||||
this.failedUpdates.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateTile()
|
||||
}
|
||||
|
||||
onPackageUpdateFailed (pack) {
|
||||
for (const update of this.failedUpdates) {
|
||||
if (update.name === pack.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.updatingPackages.length; index++) {
|
||||
const update = this.updatingPackages[index]
|
||||
if (update.name === pack.name) {
|
||||
this.updatingPackages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.failedUpdates.push(pack)
|
||||
this.updateTile()
|
||||
}
|
||||
|
||||
updateTile () {
|
||||
if (this.updates.length) {
|
||||
if (this.tooltip) {
|
||||
this.tooltip.dispose()
|
||||
this.tooltip = null
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
// Priority of -99 should put us just to the left of the Squirrel icon, which displays when Atom has updates available
|
||||
this.tile = this.statusBar.addRightTile({item: this, priority: -99})
|
||||
this.destroyed = false
|
||||
}
|
||||
|
||||
let labelText = `${_.pluralize(this.updates.length, 'update')}` // 5 updates
|
||||
let tooltipText = `${_.pluralize(this.updates.length, 'package update')} available`
|
||||
|
||||
if (this.updatingPackages.length) {
|
||||
labelText = `${this.updatingPackages.length}/${this.updates.length} updating` // 3/5 updating
|
||||
tooltipText += `, ${_.pluralize(this.updatingPackages.length, 'package')} currently updating`
|
||||
}
|
||||
|
||||
if (this.failedUpdates.length) {
|
||||
labelText += ` (${this.failedUpdates.length} failed)` // 1 update (1 failed), or 3/5 updating (1 failed)
|
||||
tooltipText += `, ${_.pluralize(this.failedUpdates.length, 'failed update')}`
|
||||
}
|
||||
|
||||
this.countLabel.textContent = labelText
|
||||
this.tooltip = atom.tooltips.add(this.element, {title: tooltipText})
|
||||
} else if (!this.destroyed) {
|
||||
this.tile.destroy()
|
||||
this.tile = null
|
||||
this.destroyed = true
|
||||
}
|
||||
}
|
||||
}
|
28
packages/settings-view/lib/rich-description.js
Normal file
28
packages/settings-view/lib/rich-description.js
Normal file
@ -0,0 +1,28 @@
|
||||
const {marked} = require('marked')
|
||||
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.code = () => ''
|
||||
renderer.blockquote = () => ''
|
||||
renderer.heading = () => ''
|
||||
renderer.html = (src) => {
|
||||
const match = src.match(/<br\s*\/?>/)
|
||||
if (match) {
|
||||
return `<br/>`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
renderer.image = () => ''
|
||||
renderer.list = () => ''
|
||||
|
||||
const markdown = text => marked(text, {renderer, breaks: true}).replace(/<p>(.*)<\/p>/, '$1').trim()
|
||||
|
||||
module.exports = {
|
||||
getSettingDescription (keyPath) {
|
||||
const schema = atom.config.getSchema(keyPath)
|
||||
let description = ''
|
||||
if (schema && schema.description) {
|
||||
description = schema.description
|
||||
}
|
||||
return markdown(description)
|
||||
}
|
||||
}
|
669
packages/settings-view/lib/settings-panel.js
Normal file
669
packages/settings-view/lib/settings-panel.js
Normal file
@ -0,0 +1,669 @@
|
||||
/** @babel */
|
||||
|
||||
import {CompositeDisposable, Disposable, TextEditor} from 'atom'
|
||||
import _ from 'underscore-plus'
|
||||
import CollapsibleSectionPanel from './collapsible-section-panel'
|
||||
import {getSettingDescription} from './rich-description'
|
||||
|
||||
const SCOPED_SETTINGS = [
|
||||
'autoIndent',
|
||||
'autoIndentOnPaste',
|
||||
'invisibles',
|
||||
'nonWordCharacters',
|
||||
'preferredLineLength',
|
||||
'scrollPastEnd',
|
||||
'showIndentGuide',
|
||||
'showInvisibles',
|
||||
'softWrap',
|
||||
'softWrapAtPreferredLineLength',
|
||||
'softWrapHangingIndent',
|
||||
'tabLength',
|
||||
'tabType'
|
||||
]
|
||||
|
||||
export default class SettingsPanel extends CollapsibleSectionPanel {
|
||||
constructor (options = {}) {
|
||||
super()
|
||||
let namespace = options.namespace
|
||||
this.element = document.createElement('section')
|
||||
this.element.classList.add('section', 'settings-panel')
|
||||
this.options = options
|
||||
this.disposables = new CompositeDisposable()
|
||||
let settings
|
||||
if (this.options.scopeName) {
|
||||
namespace = 'editor'
|
||||
settings = {}
|
||||
for (const name of SCOPED_SETTINGS) {
|
||||
settings[name] = atom.config.get(name, {scope: [this.options.scopeName]})
|
||||
}
|
||||
} else {
|
||||
settings = atom.config.get(namespace)
|
||||
}
|
||||
|
||||
this.element.appendChild(this.elementForSettings(namespace, settings))
|
||||
|
||||
this.disposables.add(this.bindInputFields())
|
||||
this.disposables.add(this.bindSelectFields())
|
||||
this.disposables.add(this.bindEditors())
|
||||
this.disposables.add(this.bindTooltips())
|
||||
this.disposables.add(this.handleEvents())
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
this.element.remove()
|
||||
}
|
||||
|
||||
elementForSettings (namespace, settings) {
|
||||
if (_.isEmpty(settings)) {
|
||||
return document.createDocumentFragment()
|
||||
}
|
||||
|
||||
let {title} = this.options
|
||||
const includeTitle = this.options.includeTitle != null ? this.options.includeTitle : true
|
||||
if (includeTitle) {
|
||||
if (title == null) {
|
||||
title = `${_.undasherize(_.uncamelcase(namespace))} Settings`
|
||||
}
|
||||
} else {
|
||||
if (title == null) {
|
||||
title = 'Settings'
|
||||
}
|
||||
}
|
||||
|
||||
const icon = this.options.icon != null ? this.options.icon : 'gear'
|
||||
const {note} = this.options
|
||||
const sortedSettings = this.sortSettings(namespace, settings)
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('section-container')
|
||||
|
||||
const heading = document.createElement('div')
|
||||
heading.classList.add('block', 'section-heading', 'icon', `icon-${icon}`)
|
||||
heading.textContent = title
|
||||
container.appendChild(heading)
|
||||
|
||||
if (note) {
|
||||
container.insertAdjacentHTML('beforeend', note)
|
||||
}
|
||||
|
||||
const body = document.createElement('div')
|
||||
body.classList.add('section-body')
|
||||
for (const name of sortedSettings) {
|
||||
body.appendChild(elementForSetting(namespace, name, settings[name]))
|
||||
}
|
||||
container.appendChild(body)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
sortSettings (namespace, settings) {
|
||||
return sortSettings(namespace, settings)
|
||||
}
|
||||
|
||||
bindInputFields () {
|
||||
const disposables = Array.from(this.element.querySelectorAll('input[id]')).map((input) => {
|
||||
let type = input.type
|
||||
let name = type === 'radio' ? input.name : input.id
|
||||
|
||||
this.observe(name, (value) => {
|
||||
if (type === 'checkbox') {
|
||||
input.checked = value
|
||||
} else if (type === 'radio') {
|
||||
input.checked = (value === this.parseValue(atom.config.getSchema(name).type, input.value))
|
||||
} else {
|
||||
if (type === 'color') {
|
||||
if (value && value.toHexString && value.toHexString()) {
|
||||
value = value.toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
input.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const changeHandler = () => {
|
||||
let value = input.value
|
||||
if (type === 'checkbox') {
|
||||
value = input.checked
|
||||
} else if (type === 'radio') {
|
||||
value = this.parseValue(atom.config.getSchema(name).type, value)
|
||||
} else {
|
||||
value = this.parseValue(type, value)
|
||||
}
|
||||
|
||||
if (type === 'color') {
|
||||
// This is debounced since the color wheel fires lots of events
|
||||
// as you are dragging it around
|
||||
clearTimeout(this.colorDebounceTimeout)
|
||||
this.colorDebounceTimeout = setTimeout(() => { this.set(name, value) }, 100)
|
||||
} else {
|
||||
this.set(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('change', changeHandler)
|
||||
return new Disposable(() => input.removeEventListener('change', changeHandler))
|
||||
})
|
||||
|
||||
return new CompositeDisposable(...disposables)
|
||||
}
|
||||
|
||||
observe (name, callback) {
|
||||
let params = {sources: [atom.config.getUserConfigPath()]}
|
||||
if (this.options.scopeName != null) {
|
||||
params.scope = [this.options.scopeName]
|
||||
}
|
||||
this.disposables.add(atom.config.observe(name, params, callback))
|
||||
}
|
||||
|
||||
isDefault (name) {
|
||||
let params = {sources: [atom.config.getUserConfigPath()]}
|
||||
if (this.options.scopeName != null) {
|
||||
params.scope = [this.options.scopeName]
|
||||
}
|
||||
let defaultValue = this.getDefault(name)
|
||||
let value = atom.config.get(name, params)
|
||||
return (value == null) || (defaultValue === value)
|
||||
}
|
||||
|
||||
getDefault (name) {
|
||||
let params = {excludeSources: [atom.config.getUserConfigPath()]}
|
||||
if (this.options.scopeName != null) {
|
||||
params.scope = [this.options.scopeName]
|
||||
}
|
||||
|
||||
let defaultValue = atom.config.get(name, params)
|
||||
if (this.options.scopeName != null) {
|
||||
// If the unscoped default is the same as the scoped default, check the actual config.cson
|
||||
// to make sure that there isn't a non-default value that is overriding the scoped value
|
||||
// For example: the default editor.tabLength is 2, but if someone sets it to 4
|
||||
// the above check still returns 2 and not 4 for a scoped editor.tabLength,
|
||||
// because it bypasses config.cson.
|
||||
if (atom.config.get(name, {excludeSources: [atom.config.getUserConfigPath()]}) === defaultValue) {
|
||||
defaultValue = atom.config.get(name)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
set (name, value) {
|
||||
if (this.options.scopeName) {
|
||||
if (value === undefined) {
|
||||
atom.config.unset(name, {scopeSelector: this.options.scopeName})
|
||||
return true
|
||||
} else {
|
||||
return atom.config.set(name, value, {scopeSelector: this.options.scopeName})
|
||||
}
|
||||
} else {
|
||||
return atom.config.set(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
setText (editor, name, type, value) {
|
||||
let stringValue
|
||||
if (this.isDefault(name)) {
|
||||
stringValue = ''
|
||||
} else {
|
||||
stringValue = this.valueToString(value) || ''
|
||||
}
|
||||
|
||||
if (stringValue === editor.getText() || _.isEqual(value, this.parseValue(type, editor.getText()))) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.setText(stringValue)
|
||||
editor.moveToEndOfLine()
|
||||
}
|
||||
|
||||
bindSelectFields () {
|
||||
const disposables = Array.from(this.element.querySelectorAll('select[id]')).map((select) => {
|
||||
const name = select.id
|
||||
this.observe(name, value => { select.value = value })
|
||||
const changeHandler = () => {
|
||||
this.set(name, select.value)
|
||||
}
|
||||
select.addEventListener('change', changeHandler)
|
||||
return new Disposable(() => select.removeEventListener('change', changeHandler))
|
||||
})
|
||||
|
||||
return new CompositeDisposable(...disposables)
|
||||
}
|
||||
|
||||
bindEditors () {
|
||||
const disposables = Array.from(this.element.querySelectorAll('atom-text-editor')).map((editorElement) => {
|
||||
let editor = editorElement.getModel()
|
||||
let name = editorElement.id
|
||||
let type = editorElement.getAttribute('type')
|
||||
let defaultValue = this.valueToString(this.getDefault(name))
|
||||
|
||||
if (defaultValue != null) {
|
||||
editor.setPlaceholderText(`Default: ${defaultValue}`)
|
||||
}
|
||||
|
||||
const subscriptions = new CompositeDisposable()
|
||||
|
||||
const focusHandler = () => {
|
||||
if (this.isDefault(name)) {
|
||||
editor.setText(this.valueToString(this.getDefault(name)) || '')
|
||||
}
|
||||
}
|
||||
editorElement.addEventListener('focus', focusHandler)
|
||||
subscriptions.add(new Disposable(() => editorElement.removeEventListener('focus', focusHandler)))
|
||||
|
||||
const blurHandler = () => {
|
||||
if (this.isDefault(name)) {
|
||||
editor.setText('')
|
||||
}
|
||||
}
|
||||
editorElement.addEventListener('blur', blurHandler)
|
||||
subscriptions.add(new Disposable(() => editorElement.removeEventListener('blur', blurHandler)))
|
||||
|
||||
this.observe(name, (value) => {
|
||||
this.setText(editor, name, type, value)
|
||||
})
|
||||
|
||||
subscriptions.add(editor.onDidStopChanging(() => {
|
||||
const {minimum, maximum} = atom.config.getSchema(name)
|
||||
const value = this.parseValue(type, editor.getText())
|
||||
if (minimum != null && value < minimum) {
|
||||
this.set(name, minimum)
|
||||
this.setText(editor, name, type, minimum)
|
||||
} else if (maximum != null && value > maximum) {
|
||||
this.set(name, maximum)
|
||||
this.setText(editor, name, type, maximum)
|
||||
} else if (!this.set(name, value)) {
|
||||
this.setText(editor, name, type, atom.config.get(name))
|
||||
}
|
||||
}))
|
||||
|
||||
return subscriptions
|
||||
})
|
||||
|
||||
return new CompositeDisposable(...disposables)
|
||||
}
|
||||
|
||||
bindTooltips () {
|
||||
const disposables = Array.from(this.element.querySelectorAll('input[id], select[id], atom-text-editor[id]')).map((element) => {
|
||||
const schema = atom.config.getSchema(element.id)
|
||||
let defaultValue = this.valueToString(this.getDefault(element.id))
|
||||
if (defaultValue != null) {
|
||||
if (schema.enum && _.findWhere(schema.enum, {value: defaultValue})) {
|
||||
defaultValue = _.findWhere(schema.enum, {value: defaultValue}).description
|
||||
}
|
||||
return atom.tooltips.add(element, {
|
||||
title: `Default: ${defaultValue}`,
|
||||
delay: {show: 100},
|
||||
placement: 'auto left'
|
||||
})
|
||||
} else {
|
||||
return new Disposable(() => {}) // no-op
|
||||
}
|
||||
})
|
||||
|
||||
return new CompositeDisposable(...disposables)
|
||||
}
|
||||
|
||||
valueToString (value) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return null
|
||||
}
|
||||
return value.map((val) => val.toString().replace(/,/g, '\\,')).join(', ')
|
||||
} else if (value != null) {
|
||||
return value.toString()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
parseValue (type, value) {
|
||||
if (value === '') {
|
||||
return undefined
|
||||
} else if (type === 'number') {
|
||||
let floatValue = parseFloat(value)
|
||||
if (isNaN(floatValue)) {
|
||||
return value
|
||||
} else {
|
||||
return floatValue
|
||||
}
|
||||
} else if (type === 'integer') {
|
||||
let intValue = parseInt(value)
|
||||
if (isNaN(intValue)) {
|
||||
return value
|
||||
} else {
|
||||
return intValue
|
||||
}
|
||||
} else if (type === 'array') {
|
||||
let arrayValue = (value || '').split(',')
|
||||
arrayValue = arrayValue.reduce((values, val) => {
|
||||
const last = values.length - 1
|
||||
if (last >= 0 && values[last].endsWith('\\')) {
|
||||
values[last] = values[last].replace(/\\$/, ',') + val
|
||||
} else {
|
||||
values.push(val)
|
||||
}
|
||||
return values
|
||||
}, [])
|
||||
return arrayValue.filter((val) => val).map((val) => val.trim())
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Space Pen Helpers
|
||||
*/
|
||||
|
||||
let isEditableArray = function (array) {
|
||||
for (let item of array) {
|
||||
if (!_.isString(item)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function sortSettings (namespace, settings) {
|
||||
return _.chain(settings)
|
||||
.keys()
|
||||
.sortBy((name) => name)
|
||||
.sortBy((name) => {
|
||||
const schema = atom.config.getSchema(`${namespace}.${name}`)
|
||||
return schema ? schema.order : null
|
||||
})
|
||||
.value()
|
||||
}
|
||||
|
||||
function elementForSetting (namespace, name, value) {
|
||||
if (namespace === 'core') {
|
||||
if (name === 'themes') { return document.createDocumentFragment() } // Handled in the Themes panel
|
||||
if (name === 'disabledPackages') { return document.createDocumentFragment() } // Handled in the Packages panel
|
||||
if (name === 'customFileTypes') { return document.createDocumentFragment() }
|
||||
if (name === 'uriHandlerRegistration') { return document.createDocumentFragment() } // Handled in the URI Handler panel
|
||||
}
|
||||
|
||||
if (namespace === 'editor') {
|
||||
// There's no global default for these, they are defined by language packages
|
||||
if (['commentStart', 'commentEnd', 'increaseIndentPattern', 'decreaseIndentPattern', 'foldEndPattern'].includes(name)) {
|
||||
return document.createDocumentFragment()
|
||||
}
|
||||
}
|
||||
|
||||
const controlGroup = document.createElement('div')
|
||||
controlGroup.classList.add('control-group')
|
||||
|
||||
const controls = document.createElement('div')
|
||||
controls.classList.add('controls')
|
||||
controlGroup.appendChild(controls)
|
||||
|
||||
let schema = atom.config.getSchema(`${namespace}.${name}`)
|
||||
if (schema && schema.enum) {
|
||||
controls.appendChild(elementForOptions(namespace, name, value, {radio: schema.radio}))
|
||||
} else if (schema && schema.type === 'color') {
|
||||
controls.appendChild(elementForColor(namespace, name, value))
|
||||
} else if (_.isBoolean(value) || (schema && schema.type === 'boolean')) {
|
||||
controls.appendChild(elementForCheckbox(namespace, name, value))
|
||||
} else if (_.isArray(value) || (schema && schema.type === 'array')) {
|
||||
if (isEditableArray(value)) {
|
||||
controls.appendChild(elementForArray(namespace, name, value))
|
||||
}
|
||||
} else if (_.isObject(value) || (schema && schema.type === 'object')) {
|
||||
controls.appendChild(elementForObject(namespace, name, value))
|
||||
} else {
|
||||
controls.appendChild(elementForEditor(namespace, name, value))
|
||||
}
|
||||
|
||||
return controlGroup
|
||||
}
|
||||
|
||||
function getSettingTitle (keyPath, name) {
|
||||
if (name == null) {
|
||||
name = ''
|
||||
}
|
||||
const schema = atom.config.getSchema(keyPath)
|
||||
const title = schema != null ? schema.title : null
|
||||
return title || _.uncamelcase(name).split('.').map(_.capitalize).join(' ')
|
||||
}
|
||||
|
||||
function elementForOptions (namespace, name, value, {radio = false}) {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
let schema = atom.config.getSchema(keyPath)
|
||||
let options = (schema && schema.enum) ? schema.enum : []
|
||||
|
||||
const fragment = document.createDocumentFragment()
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.classList.add('control-label')
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.classList.add('setting-title')
|
||||
titleDiv.textContent = getSettingTitle(keyPath, name)
|
||||
label.appendChild(titleDiv)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
label.appendChild(descriptionDiv)
|
||||
|
||||
fragment.appendChild(label)
|
||||
fragment.appendChild(enumOptions(options, {keyPath, radio}))
|
||||
|
||||
return fragment
|
||||
}
|
||||
|
||||
function elementForCheckbox (namespace, name, value) {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('checkbox')
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.for = keyPath
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.id = keyPath
|
||||
input.type = 'checkbox'
|
||||
input.classList.add('input-checkbox')
|
||||
label.appendChild(input)
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.classList.add('setting-title')
|
||||
titleDiv.textContent = getSettingTitle(keyPath, name)
|
||||
label.appendChild(titleDiv)
|
||||
div.appendChild(label)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
div.appendChild(descriptionDiv)
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
function elementForColor (namespace, name, value) {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('color')
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.for = keyPath
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.id = keyPath
|
||||
input.type = 'color'
|
||||
label.appendChild(input)
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.classList.add('setting-title')
|
||||
titleDiv.textContent = getSettingTitle(keyPath, name)
|
||||
label.appendChild(titleDiv)
|
||||
div.appendChild(label)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
div.appendChild(descriptionDiv)
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
function elementForEditor (namespace, name, value) {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
let type = _.isNumber(value) ? 'number' : 'string'
|
||||
|
||||
const fragment = document.createDocumentFragment()
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.classList.add('control-label')
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.classList.add('setting-title')
|
||||
titleDiv.textContent = getSettingTitle(keyPath, name)
|
||||
label.appendChild(titleDiv)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
label.appendChild(descriptionDiv)
|
||||
fragment.appendChild(label)
|
||||
|
||||
const controls = document.createElement('div')
|
||||
controls.classList.add('controls')
|
||||
|
||||
const editorContainer = document.createElement('div')
|
||||
editorContainer.classList.add('editor-container')
|
||||
|
||||
const editor = new TextEditor({mini: true})
|
||||
editor.element.id = keyPath
|
||||
editor.element.setAttribute('type', type)
|
||||
editorContainer.appendChild(editor.element)
|
||||
controls.appendChild(editorContainer)
|
||||
fragment.appendChild(controls)
|
||||
|
||||
return fragment
|
||||
}
|
||||
|
||||
function elementForArray (namespace, name, value) {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
|
||||
const fragment = document.createDocumentFragment()
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.classList.add('control-label')
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.classList.add('setting-title')
|
||||
titleDiv.textContent = getSettingTitle(keyPath, name)
|
||||
label.appendChild(titleDiv)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
label.appendChild(descriptionDiv)
|
||||
fragment.appendChild(label)
|
||||
|
||||
const controls = document.createElement('div')
|
||||
controls.classList.add('controls')
|
||||
|
||||
const editorContainer = document.createElement('div')
|
||||
editorContainer.classList.add('editor-container')
|
||||
|
||||
const editor = new TextEditor({mini: true})
|
||||
editor.element.id = keyPath
|
||||
editor.element.setAttribute('type', 'array')
|
||||
editorContainer.appendChild(editor.element)
|
||||
controls.appendChild(editorContainer)
|
||||
fragment.appendChild(controls)
|
||||
|
||||
return fragment
|
||||
}
|
||||
|
||||
function elementForObject (namespace, name, value) {
|
||||
if (_.keys(value).length === 0) {
|
||||
return document.createDocumentFragment()
|
||||
} else {
|
||||
let keyPath = `${namespace}.${name}`
|
||||
let schema = atom.config.getSchema(keyPath)
|
||||
let isCollapsed = schema.collapsed === true
|
||||
|
||||
const section = document.createElement('section')
|
||||
section.classList.add('sub-section')
|
||||
if (isCollapsed) {
|
||||
section.classList.add('collapsed')
|
||||
}
|
||||
|
||||
const h3 = document.createElement('h3')
|
||||
h3.classList.add('sub-section-heading', 'has-items')
|
||||
h3.textContent = getSettingTitle(keyPath, name)
|
||||
section.appendChild(h3)
|
||||
|
||||
const descriptionDiv = document.createElement('div')
|
||||
descriptionDiv.classList.add('setting-description')
|
||||
descriptionDiv.innerHTML = getSettingDescription(keyPath)
|
||||
section.appendChild(descriptionDiv)
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('sub-section-body')
|
||||
for (const key of sortSettings(keyPath, value)) {
|
||||
div.appendChild(elementForSetting(namespace, `${name}.${key}`, value[key]))
|
||||
}
|
||||
section.appendChild(div)
|
||||
|
||||
return section
|
||||
}
|
||||
}
|
||||
|
||||
function enumOptions (options, {keyPath, radio}) {
|
||||
const containerTag = radio ? 'fieldset' : 'select'
|
||||
const container = document.createElement(containerTag)
|
||||
container.id = keyPath
|
||||
const containerClass = radio ? 'input-radio-group' : 'form-control'
|
||||
container.classList.add(containerClass)
|
||||
|
||||
const conversion = radio ? optionToRadio : optionToSelect
|
||||
const optionElements = options.map(option => conversion(option, keyPath))
|
||||
|
||||
for (const optionElement of optionElements) { container.appendChild(optionElement) }
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
function optionToRadio (option, keyPath) {
|
||||
const button = document.createElement('input')
|
||||
const label = document.createElement('label')
|
||||
label.classList.add('input-label')
|
||||
let value
|
||||
let description = ''
|
||||
if (option.hasOwnProperty('value')) {
|
||||
value = option.value
|
||||
description = option.description
|
||||
} else {
|
||||
value = option
|
||||
description = option
|
||||
}
|
||||
button.classList.add('input-radio')
|
||||
button.id = `${keyPath}[${value}]`
|
||||
button.name = keyPath
|
||||
button.type = 'radio'
|
||||
button.value = value
|
||||
label.appendChild(button)
|
||||
label.appendChild(document.createTextNode(description))
|
||||
return label
|
||||
}
|
||||
|
||||
function optionToSelect (option, keyPath) {
|
||||
const optionElement = document.createElement('option')
|
||||
if (option.hasOwnProperty('value')) {
|
||||
optionElement.value = option.value
|
||||
optionElement.textContent = option.description
|
||||
} else {
|
||||
optionElement.value = option
|
||||
optionElement.textContent = option
|
||||
}
|
||||
return optionElement
|
||||
}
|
394
packages/settings-view/lib/settings-view.js
Normal file
394
packages/settings-view/lib/settings-view.js
Normal file
@ -0,0 +1,394 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import path from 'path'
|
||||
import etch from 'etch'
|
||||
import _ from 'underscore-plus'
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
|
||||
import GeneralPanel from './general-panel'
|
||||
import EditorPanel from './editor-panel'
|
||||
import PackageDetailView from './package-detail-view'
|
||||
import KeybindingsPanel from './keybindings-panel'
|
||||
import InstallPanel from './install-panel'
|
||||
import ThemesPanel from './themes-panel'
|
||||
import InstalledPackagesPanel from './installed-packages-panel'
|
||||
import UpdatesPanel from './updates-panel'
|
||||
import UriHandlerPanel from './uri-handler-panel'
|
||||
|
||||
export default class SettingsView {
|
||||
constructor ({uri, packageManager, snippetsProvider, activePanel} = {}) {
|
||||
this.uri = uri
|
||||
this.packageManager = packageManager
|
||||
this.snippetsProvider = snippetsProvider
|
||||
this.deferredPanel = activePanel
|
||||
this.destroyed = false
|
||||
this.panelsByName = {}
|
||||
this.panelCreateCallbacks = {}
|
||||
|
||||
etch.initialize(this)
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
this.disposables.add(atom.packages.onDidActivateInitialPackages(() => {
|
||||
this.disposables.add(
|
||||
atom.packages.onDidActivatePackage(pack => this.removePanelCache(pack.name)),
|
||||
atom.packages.onDidDeactivatePackage(pack => this.removePanelCache(pack.name))
|
||||
)
|
||||
}))
|
||||
|
||||
process.nextTick(() => this.initializePanels())
|
||||
}
|
||||
|
||||
removePanelCache (name) {
|
||||
delete this.panelsByName[name]
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
destroy () {
|
||||
this.destroyed = true
|
||||
this.disposables.dispose()
|
||||
for (let name in this.panelsByName) {
|
||||
const panel = this.panelsByName[name]
|
||||
panel.destroy()
|
||||
}
|
||||
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='settings-view pane-item' tabIndex='-1'>
|
||||
<div className='config-menu' ref='sidebar'>
|
||||
<ul className='panels-menu nav nav-pills nav-stacked' ref='panelMenu'>
|
||||
<div className='panel-menu-separator' ref='menuSeparator' />
|
||||
</ul>
|
||||
<div className='button-area'>
|
||||
<button className='btn btn-default icon icon-link-external' ref='openDotAtom'>Open Config Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* The tabindex attr below ensures that clicks in a panel item won't
|
||||
cause this view to gain focus. This is important because when this view
|
||||
gains focus (e.g. immediately after atom displays it), it focuses the
|
||||
currently active panel item. If that focusing causes the active panel to
|
||||
scroll (e.g. because the active panel itself passes focus on to a search
|
||||
box at the top of a scrolled panel), then the browser will not fire the
|
||||
click event on the element within the panel on which the user originally
|
||||
clicked (e.g. a package card). This would prevent us from showing a
|
||||
package detail view when clicking on a package card. Phew! */}
|
||||
<div className='panels' tabIndex='-1' ref='panels' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// This prevents the view being actually disposed when closed
|
||||
// If you remove it you will need to ensure the cached settingsView
|
||||
// in main.coffee is correctly released on close as well...
|
||||
onDidChangeTitle () { return new Disposable() }
|
||||
|
||||
initializePanels () {
|
||||
if (this.refs.panels.children.length > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const clickHandler = (event) => {
|
||||
const target = event.target.closest('.panels-menu li a, .panels-packages li a')
|
||||
if (target) {
|
||||
this.showPanel(target.closest('li').name)
|
||||
}
|
||||
}
|
||||
this.element.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => this.element.removeEventListener('click', clickHandler)))
|
||||
|
||||
const focusHandler = () => {
|
||||
this.focusActivePanel()
|
||||
}
|
||||
this.element.addEventListener('focus', focusHandler)
|
||||
this.disposables.add(new Disposable(() => this.element.removeEventListener('focus', focusHandler)))
|
||||
|
||||
const openDotAtomClickHandler = () => {
|
||||
atom.open({pathsToOpen: [atom.getConfigDirPath()]})
|
||||
}
|
||||
this.refs.openDotAtom.addEventListener('click', openDotAtomClickHandler)
|
||||
this.disposables.add(new Disposable(() => this.refs.openDotAtom.removeEventListener('click', openDotAtomClickHandler)))
|
||||
|
||||
this.addCorePanel('Core', 'settings', () => new GeneralPanel())
|
||||
this.addCorePanel('Editor', 'code', () => new EditorPanel())
|
||||
if (atom.config.getSchema('core.uriHandlerRegistration').type !== 'any') {
|
||||
// "feature flag" based on core support for URI handling
|
||||
this.addCorePanel('URI Handling', 'link', () => new UriHandlerPanel())
|
||||
}
|
||||
if ((process.platform === 'win32') && (require('atom').WinShell != null)) {
|
||||
const SystemPanel = require('./system-windows-panel')
|
||||
this.addCorePanel('System', 'device-desktop', () => new SystemPanel())
|
||||
}
|
||||
this.addCorePanel('Keybindings', 'keyboard', () => new KeybindingsPanel())
|
||||
this.addCorePanel('Packages', 'package', () => new InstalledPackagesPanel(this, this.packageManager))
|
||||
this.addCorePanel('Themes', 'paintcan', () => new ThemesPanel(this, this.packageManager))
|
||||
this.addCorePanel('Updates', 'cloud-download', () => new UpdatesPanel(this, this.packageManager))
|
||||
this.addCorePanel('Install', 'plus', () => new InstallPanel(this, this.packageManager))
|
||||
|
||||
this.showDeferredPanel()
|
||||
|
||||
if (!this.activePanel) {
|
||||
this.showPanel('Core')
|
||||
}
|
||||
|
||||
if (document.body.contains(this.element)) {
|
||||
this.refs.sidebar.style.width = this.refs.sidebar.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
serialize () {
|
||||
return {
|
||||
deserializer: 'SettingsView',
|
||||
version: 2,
|
||||
activePanel: this.activePanel != null ? this.activePanel : this.deferredPanel,
|
||||
uri: this.uri
|
||||
}
|
||||
}
|
||||
|
||||
getPackages () {
|
||||
let bundledPackageMetadataCache
|
||||
if (this.packages != null) { return this.packages }
|
||||
|
||||
this.packages = atom.packages.getLoadedPackages()
|
||||
|
||||
try {
|
||||
const packageMetadata = require(path.join(atom.getLoadSettings().resourcePath, 'package.json'))
|
||||
bundledPackageMetadataCache = packageMetadata ? packageMetadata._atomPackages : null
|
||||
} catch (error) {}
|
||||
|
||||
// Include disabled packages so they can be re-enabled from the UI
|
||||
const disabledPackages = atom.config.get('core.disabledPackages') || []
|
||||
for (const packageName of disabledPackages) {
|
||||
var metadata
|
||||
const packagePath = atom.packages.resolvePackagePath(packageName)
|
||||
if (!packagePath) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
metadata = require(path.join(packagePath, 'package.json'))
|
||||
} catch (error) {
|
||||
if (bundledPackageMetadataCache && bundledPackageMetadataCache[packageName]) {
|
||||
metadata = bundledPackageMetadataCache[packageName].metadata
|
||||
}
|
||||
}
|
||||
if (metadata == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const name = metadata.name != null ? metadata.name : packageName
|
||||
if (!_.findWhere(this.packages, {name})) {
|
||||
this.packages.push({name, metadata, path: packagePath})
|
||||
}
|
||||
}
|
||||
|
||||
this.packages.sort((pack1, pack2) => {
|
||||
const title1 = this.packageManager.getPackageTitle(pack1)
|
||||
const title2 = this.packageManager.getPackageTitle(pack2)
|
||||
return title1.localeCompare(title2)
|
||||
})
|
||||
|
||||
return this.packages
|
||||
}
|
||||
|
||||
addCorePanel (name, iconName, panelCreateCallback) {
|
||||
const panelMenuItem = document.createElement('li')
|
||||
panelMenuItem.name = name
|
||||
panelMenuItem.setAttribute('name', name)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.classList.add('icon', `icon-${iconName}`)
|
||||
a.textContent = name
|
||||
panelMenuItem.appendChild(a)
|
||||
|
||||
this.refs.menuSeparator.parentElement.insertBefore(panelMenuItem, this.refs.menuSeparator)
|
||||
this.addPanel(name, panelCreateCallback)
|
||||
}
|
||||
|
||||
addPanel (name, panelCreateCallback) {
|
||||
this.panelCreateCallbacks[name] = panelCreateCallback
|
||||
if (this.deferredPanel && this.deferredPanel.name === name) {
|
||||
this.showDeferredPanel()
|
||||
}
|
||||
}
|
||||
|
||||
getOrCreatePanel (name, options) {
|
||||
let panel = this.panelsByName[name]
|
||||
if (panel) return panel
|
||||
|
||||
if (name in this.panelCreateCallbacks) {
|
||||
panel = this.panelCreateCallbacks[name]()
|
||||
delete this.panelCreateCallbacks[name]
|
||||
} else if (options && options.pack) {
|
||||
if (!options.pack.metadata) {
|
||||
options.pack.metadata = _.clone(options.pack)
|
||||
}
|
||||
panel = new PackageDetailView(options.pack, this, this.packageManager, this.snippetsProvider)
|
||||
}
|
||||
if (panel) {
|
||||
this.panelsByName[name] = panel
|
||||
}
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
makePanelMenuActive (name) {
|
||||
const previouslyActivePanel = this.refs.sidebar.querySelector('.active')
|
||||
if (previouslyActivePanel) {
|
||||
previouslyActivePanel.classList.remove('active')
|
||||
}
|
||||
|
||||
const newActivePanel = this.refs.sidebar.querySelector(`[name='${name}']`)
|
||||
if (newActivePanel) {
|
||||
newActivePanel.classList.add('active')
|
||||
}
|
||||
}
|
||||
|
||||
focusActivePanel () {
|
||||
// Pass focus to panel that is currently visible
|
||||
for (let i = 0; i < this.refs.panels.children.length; i++) {
|
||||
const child = this.refs.panels.children[i]
|
||||
if (child.offsetWidth > 0) {
|
||||
child.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showDeferredPanel () {
|
||||
if (this.deferredPanel) {
|
||||
const {name, options} = this.deferredPanel
|
||||
this.showPanel(name, options)
|
||||
}
|
||||
}
|
||||
|
||||
// Public: show a panel.
|
||||
//
|
||||
// * `name` {String} the name of the panel to show
|
||||
// * `options` {Object} an options hash. Will be passed to `beforeShow()` on
|
||||
// the panel. Options may include (but are not limited to):
|
||||
// * `uri` the URI the panel was launched from
|
||||
showPanel (name, options) {
|
||||
if (this.activePanel) {
|
||||
const prev = this.panelsByName[this.activePanel.name]
|
||||
if (prev) {
|
||||
prev.scrollPosition = prev.element.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
const panel = this.getOrCreatePanel(name, options)
|
||||
if (panel) {
|
||||
this.appendPanel(panel, options)
|
||||
this.makePanelMenuActive(name)
|
||||
this.setActivePanel(name, options)
|
||||
this.deferredPanel = null
|
||||
} else {
|
||||
this.deferredPanel = {name, options}
|
||||
}
|
||||
}
|
||||
|
||||
showPanelForURI (uri) {
|
||||
const regex = /config\/([a-z]+)\/?([a-zA-Z0-9_-]+)?/i
|
||||
const match = regex.exec(uri)
|
||||
|
||||
if (match) {
|
||||
const path1 = match[1]
|
||||
const path2 = match[2]
|
||||
|
||||
if (path1 === 'packages' && path2 != null) {
|
||||
this.showPanel(path2, {
|
||||
uri: uri,
|
||||
pack: {name: path2},
|
||||
back: atom.packages.getLoadedPackage(path2) ? 'Packages' : null
|
||||
})
|
||||
} else {
|
||||
const panelName = path1[0].toUpperCase() + path1.slice(1)
|
||||
this.showPanel(panelName, {uri})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendPanel (panel, options) {
|
||||
for (let i = 0; i < this.refs.panels.children.length; i++) {
|
||||
this.refs.panels.children[i].style.display = 'none'
|
||||
}
|
||||
|
||||
if (!this.refs.panels.contains(panel.element)) {
|
||||
this.refs.panels.appendChild(panel.element)
|
||||
}
|
||||
|
||||
if (panel.beforeShow) {
|
||||
panel.beforeShow(options)
|
||||
}
|
||||
panel.show()
|
||||
panel.focus()
|
||||
}
|
||||
|
||||
setActivePanel (name, options = {}) {
|
||||
this.activePanel = {name, options}
|
||||
|
||||
const panel = this.panelsByName[name]
|
||||
if (panel && panel.scrollPosition) {
|
||||
panel.element.scrollTop = panel.scrollPosition
|
||||
delete panel.scrollPosition
|
||||
}
|
||||
}
|
||||
|
||||
removePanel (name) {
|
||||
const panel = this.panelsByName[name]
|
||||
if (panel) {
|
||||
panel.destroy()
|
||||
delete this.panelsByName[name]
|
||||
}
|
||||
}
|
||||
|
||||
getTitle () {
|
||||
return 'Settings'
|
||||
}
|
||||
|
||||
getIconName () {
|
||||
return 'tools'
|
||||
}
|
||||
|
||||
getURI () {
|
||||
return this.uri
|
||||
}
|
||||
|
||||
isEqual (other) {
|
||||
return other instanceof SettingsView
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
145
packages/settings-view/lib/system-windows-panel.js
Normal file
145
packages/settings-view/lib/system-windows-panel.js
Normal file
@ -0,0 +1,145 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {WinShell, CompositeDisposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
|
||||
export default class SystemPanel {
|
||||
constructor () {
|
||||
etch.initialize(this)
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
WinShell.fileHandler.isRegistered((i) => { this.refs.fileHandlerCheckbox.checked = i })
|
||||
WinShell.fileContextMenu.isRegistered((i) => { this.refs.fileContextMenuCheckbox.checked = i })
|
||||
WinShell.folderContextMenu.isRegistered((i) => { this.refs.folderContextMenuCheckbox.checked = i })
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='panels-item' tabIndex='0'>
|
||||
<form className='general-panel section'>
|
||||
<div className='settings-panel'>
|
||||
<div className='section-container'>
|
||||
<div className='block section-heading icon icon-device-desktop'>System Settings</div>
|
||||
<div className='text icon icon-question'>These settings determine how Atom integrates with your operating system.</div>
|
||||
<div className='section-body'>
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<div className='checkbox'>
|
||||
<label for='system.windows.file-handler'>
|
||||
<input
|
||||
ref='fileHandlerCheckbox'
|
||||
id='system.windows.file-handler'
|
||||
className='input-checkbox'
|
||||
type='checkbox'
|
||||
onclick={(e) => { this.setRegistration(WinShell.fileHandler, e.target.checked) }} />
|
||||
<div className='setting-title'>Register as file handler</div>
|
||||
<div className='setting-description'>
|
||||
Show {WinShell.appName} in the "Open with" application list for easy association with file types.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<div className='checkbox'>
|
||||
<label for='system.windows.shell-menu-files'>
|
||||
<input
|
||||
ref='fileContextMenuCheckbox'
|
||||
id='system.windows.shell-menu-files'
|
||||
className='input-checkbox'
|
||||
type='checkbox'
|
||||
onclick={(e) => { this.setRegistration(WinShell.fileContextMenu, e.target.checked) }} />
|
||||
<div className='setting-title'>Show in file context menus</div>
|
||||
<div className='setting-description'>
|
||||
Add "Open with {WinShell.appName}" to the File Explorer context menu for files.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<div className='checkbox'>
|
||||
<label for='system.windows.shell-menu-folders'>
|
||||
<input
|
||||
ref='folderContextMenuCheckbox'
|
||||
id='system.windows.shell-menu-folders'
|
||||
className='input-checkbox'
|
||||
type='checkbox'
|
||||
onclick={(e) => {
|
||||
this.setRegistration(WinShell.folderContextMenu, e.target.checked)
|
||||
this.setRegistration(WinShell.folderBackgroundContextMenu, e.target.checked)
|
||||
}} />
|
||||
<div className='setting-title'>Show in folder context menus</div>
|
||||
<div className='setting-description'>
|
||||
Add "Open with {WinShell.appName}" to the File Explorer context menu for folders.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
setRegistration (option, shouldBeRegistered) {
|
||||
if (shouldBeRegistered) {
|
||||
return option.register(function () {})
|
||||
} else {
|
||||
return option.deregister(function () {})
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
484
packages/settings-view/lib/themes-panel.js
Normal file
484
packages/settings-view/lib/themes-panel.js
Normal file
@ -0,0 +1,484 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import fuzzaldrin from 'fuzzaldrin'
|
||||
import etch from 'etch'
|
||||
import _ from 'underscore-plus'
|
||||
import {CompositeDisposable, TextEditor} from 'atom'
|
||||
|
||||
import CollapsibleSectionPanel from './collapsible-section-panel'
|
||||
import PackageCard from './package-card'
|
||||
import ErrorView from './error-view'
|
||||
|
||||
import List from './list'
|
||||
import ListView from './list-view'
|
||||
import {ownerFromRepository, packageComparatorAscending} from './utils'
|
||||
|
||||
export default class ThemesPanel extends CollapsibleSectionPanel {
|
||||
static loadPackagesDelay () {
|
||||
return 300
|
||||
}
|
||||
|
||||
constructor (settingsView, packageManager) {
|
||||
super()
|
||||
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
etch.initialize(this)
|
||||
this.items = {
|
||||
dev: new List('name'),
|
||||
core: new List('name'),
|
||||
user: new List('name'),
|
||||
git: new List('name')
|
||||
}
|
||||
this.itemViews = {
|
||||
dev: new ListView(this.items.dev, this.refs.devPackages, this.createPackageCard.bind(this)),
|
||||
core: new ListView(this.items.core, this.refs.corePackages, this.createPackageCard.bind(this)),
|
||||
user: new ListView(this.items.user, this.refs.communityPackages, this.createPackageCard.bind(this)),
|
||||
git: new ListView(this.items.git, this.refs.gitPackages, this.createPackageCard.bind(this))
|
||||
}
|
||||
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.disposables.add(
|
||||
this.packageManager.on('theme-install-failed theme-uninstall-failed', ({pack, error}) => {
|
||||
this.refs.themeErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
})
|
||||
)
|
||||
this.disposables.add(this.handleEvents())
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
this.loadPackages()
|
||||
|
||||
this.disposables.add(
|
||||
this.packageManager.on('theme-installed theme-uninstalled', () => {
|
||||
let loadPackagesTimeout
|
||||
clearTimeout(loadPackagesTimeout)
|
||||
loadPackagesTimeout = setTimeout(() => {
|
||||
this.populateThemeMenus()
|
||||
this.loadPackages()
|
||||
}, ThemesPanel.loadPackagesDelay())
|
||||
})
|
||||
)
|
||||
|
||||
this.disposables.add(atom.themes.onDidChangeActiveThemes(() => this.updateActiveThemes()))
|
||||
this.disposables.add(atom.tooltips.add(this.refs.activeUiThemeSettings, {title: 'Settings'}))
|
||||
this.disposables.add(atom.tooltips.add(this.refs.activeSyntaxThemeSettings, {title: 'Settings'}))
|
||||
this.updateActiveThemes()
|
||||
|
||||
this.disposables.add(this.refs.filterEditor.onDidStopChanging(() => { this.matchPackages() }))
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
focus () {
|
||||
this.refs.filterEditor.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='panels-item' tabIndex='-1'>
|
||||
<div className='section packages themes-panel'>
|
||||
<div className='section-container'>
|
||||
<div className='section-heading icon icon-paintcan'>Choose a Theme</div>
|
||||
|
||||
<div className='text native-key-bindings' tabIndex='-1'>
|
||||
<span className='icon icon-question'>You can also style Atom by editing </span>
|
||||
<a className='link' onclick={this.didClickOpenUserStyleSheet}>your stylesheet</a>
|
||||
</div>
|
||||
|
||||
<div className='themes-picker'>
|
||||
<div className='themes-picker-item control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='setting-title themes-label text'>UI Theme</div>
|
||||
<div className='setting-description text theme-description'>This styles the tabs, status bar, tree view, and dropdowns</div>
|
||||
</label>
|
||||
<div className='select-container'>
|
||||
<select ref='uiMenu' className='form-control' onchange={this.didChangeUiMenu.bind(this)} />
|
||||
<button
|
||||
ref='activeUiThemeSettings'
|
||||
className='btn icon icon-gear active-theme-settings'
|
||||
onclick={this.didClickActiveUiThemeSettings.bind(this)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='themes-picker-item control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='setting-title themes-label text'>Syntax Theme</div>
|
||||
<div className='setting-description text theme-description'>This styles the text inside the editor</div>
|
||||
</label>
|
||||
<div className='select-container'>
|
||||
<select ref='syntaxMenu' className='form-control' onchange={this.didChangeSyntaxMenu.bind(this)} />
|
||||
<button
|
||||
ref='activeSyntaxThemeSettings'
|
||||
className='btn icon icon-gear active-syntax-settings'
|
||||
onclick={this.didClickActiveSyntaxThemeSettings.bind(this)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className='section'>
|
||||
<div className='section-container'>
|
||||
<div className='section-heading icon icon-paintcan'>
|
||||
Installed Themes
|
||||
<span ref='totalPackages' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</div>
|
||||
<div className='editor-container'>
|
||||
<TextEditor ref='filterEditor' mini placeholderText='Filter themes by name' />
|
||||
</div>
|
||||
|
||||
<div ref='themeErrors' />
|
||||
|
||||
<section className='sub-section installed-packages'>
|
||||
<h3 ref='communityThemesHeader' className='sub-section-heading icon icon-paintcan'>
|
||||
Community Themes
|
||||
<span ref='communityCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='communityPackages' className='container package-container'>
|
||||
<div ref='communityLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading themes…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section core-packages'>
|
||||
<h3 ref='coreThemesHeader' className='sub-section-heading icon icon-paintcan'>
|
||||
Core Themes
|
||||
<span ref='coreCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='corePackages' className='container package-container'>
|
||||
<div ref='coreLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading themes…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section dev-packages'>
|
||||
<h3 ref='developmentThemesHeader' className='sub-section-heading icon icon-paintcan'>
|
||||
Development Themes
|
||||
<span ref='devCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='devPackages' className='container package-container'>
|
||||
<div ref='devLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading themes…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='sub-section git-packages'>
|
||||
<h3 ref='gitThemesHeader' className='sub-section-heading icon icon-paintcan'>
|
||||
Git Themes
|
||||
<span ref='gitCount' className='section-heading-count badge badge-flexible'>…</span>
|
||||
</h3>
|
||||
<div ref='gitPackages' className='container package-container'>
|
||||
<div ref='gitLoadingArea' className='alert alert-info loading-area icon icon-hourglass'>Loading themes…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
filterThemes (packages) {
|
||||
packages.dev = packages.dev.filter(({theme}) => theme)
|
||||
packages.user = packages.user.filter(({theme}) => theme)
|
||||
packages.core = packages.core.filter(({theme}) => theme)
|
||||
packages.git = (packages.git || []).filter(({theme}) => theme)
|
||||
|
||||
for (let pack of packages.core) {
|
||||
if (pack.repository == null) {
|
||||
pack.repository = `https://github.com/atom/${pack.name}`
|
||||
}
|
||||
}
|
||||
|
||||
for (let packageType of ['dev', 'core', 'user', 'git']) {
|
||||
for (let pack of packages[packageType]) {
|
||||
pack.owner = ownerFromRepository(pack.repository)
|
||||
}
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
sortThemes (packages) {
|
||||
packages.dev.sort(packageComparatorAscending)
|
||||
packages.core.sort(packageComparatorAscending)
|
||||
packages.user.sort(packageComparatorAscending)
|
||||
packages.git.sort(packageComparatorAscending)
|
||||
return packages
|
||||
}
|
||||
|
||||
loadPackages () {
|
||||
this.packageViews = []
|
||||
this.packageManager.getInstalled().then(packages => {
|
||||
this.packages = this.sortThemes(this.filterThemes(packages))
|
||||
|
||||
this.refs.devLoadingArea.remove()
|
||||
this.items.dev.setItems(this.packages.dev)
|
||||
|
||||
this.refs.coreLoadingArea.remove()
|
||||
this.items.core.setItems(this.packages.core)
|
||||
|
||||
this.refs.communityLoadingArea.remove()
|
||||
this.items.user.setItems(this.packages.user)
|
||||
|
||||
this.refs.gitLoadingArea.remove()
|
||||
this.items.git.setItems(this.packages.git)
|
||||
|
||||
// TODO show empty mesage per section
|
||||
|
||||
this.updateSectionCounts()
|
||||
}).catch((error) => {
|
||||
this.refs.themeErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
})
|
||||
}
|
||||
|
||||
// Update the active UI and syntax themes and populate the menu
|
||||
updateActiveThemes () {
|
||||
this.activeUiTheme = this.getActiveUiTheme()
|
||||
this.activeSyntaxTheme = this.getActiveSyntaxTheme()
|
||||
this.populateThemeMenus()
|
||||
this.toggleActiveThemeButtons()
|
||||
}
|
||||
|
||||
toggleActiveThemeButtons () {
|
||||
if (this.hasSettings(this.activeUiTheme)) {
|
||||
this.refs.activeUiThemeSettings.style.display = ''
|
||||
} else {
|
||||
this.refs.activeUiThemeSettings.style.display = 'none'
|
||||
}
|
||||
|
||||
if (this.hasSettings(this.activeSyntaxTheme)) {
|
||||
this.refs.activeSyntaxThemeSettings.display = ''
|
||||
} else {
|
||||
this.refs.activeSyntaxThemeSettings.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
hasSettings (packageName) {
|
||||
return this.packageManager.packageHasSettings(packageName)
|
||||
}
|
||||
|
||||
// Populate the theme menus from the theme manager's active themes
|
||||
populateThemeMenus () {
|
||||
this.refs.uiMenu.innerHTML = ''
|
||||
this.refs.syntaxMenu.innerHTML = ''
|
||||
const availableThemes = _.sortBy(atom.themes.getLoadedThemes(), 'name')
|
||||
for (let {name, metadata} of availableThemes) {
|
||||
switch (metadata.theme) {
|
||||
case 'ui': {
|
||||
const themeItem = this.createThemeMenuItem(name)
|
||||
if (name === this.activeUiTheme) {
|
||||
themeItem.selected = true
|
||||
}
|
||||
this.refs.uiMenu.appendChild(themeItem)
|
||||
break
|
||||
}
|
||||
case 'syntax': {
|
||||
const themeItem = this.createThemeMenuItem(name)
|
||||
if (name === this.activeSyntaxTheme) {
|
||||
themeItem.selected = true
|
||||
}
|
||||
this.refs.syntaxMenu.appendChild(themeItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the name of the active ui theme.
|
||||
getActiveUiTheme () {
|
||||
for (let {name, metadata} of atom.themes.getActiveThemes()) {
|
||||
if (metadata.theme === 'ui') {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the name of the active syntax theme.
|
||||
getActiveSyntaxTheme () {
|
||||
for (let {name, metadata} of atom.themes.getActiveThemes()) {
|
||||
if (metadata.theme === 'syntax') { return name }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Update the config with the selected themes
|
||||
updateThemeConfig () {
|
||||
const themes = []
|
||||
if (this.activeUiTheme) {
|
||||
themes.push(this.activeUiTheme)
|
||||
}
|
||||
if (this.activeSyntaxTheme) {
|
||||
themes.push(this.activeSyntaxTheme)
|
||||
}
|
||||
if (themes.length > 0) {
|
||||
atom.config.set('core.themes', themes)
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUpdateThemeConfig () {
|
||||
setTimeout(() => { this.updateThemeConfig() }, 100)
|
||||
}
|
||||
|
||||
// Create a menu item for the given theme name.
|
||||
createThemeMenuItem (themeName) {
|
||||
const title = _.undasherize(_.uncamelcase(themeName.replace(/-(ui|syntax)/g, '').replace(/-theme$/g, '')))
|
||||
const option = document.createElement('option')
|
||||
option.value = themeName
|
||||
option.textContent = title
|
||||
return option
|
||||
}
|
||||
|
||||
createPackageCard (pack) {
|
||||
return new PackageCard(pack, this.settingsView, this.packageManager, {back: 'Themes'})
|
||||
}
|
||||
|
||||
filterPackageListByText (text) {
|
||||
if (!this.packages) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let packageType of ['dev', 'core', 'user', 'git']) {
|
||||
const allViews = this.itemViews[packageType].getViews()
|
||||
const activeViews = this.itemViews[packageType].filterViews((pack) => {
|
||||
if (text === '') {
|
||||
return true
|
||||
} else {
|
||||
const owner = pack.owner != null ? pack.owner : ownerFromRepository(pack.repository)
|
||||
const filterText = `${pack.name} ${owner}`
|
||||
return fuzzaldrin.score(filterText, text) > 0
|
||||
}
|
||||
})
|
||||
|
||||
for (const view of allViews) {
|
||||
if (view) {
|
||||
view.element.style.display = 'none'
|
||||
view.element.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
for (const view of activeViews) {
|
||||
if (view) {
|
||||
view.element.style.display = ''
|
||||
view.element.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSectionCounts()
|
||||
}
|
||||
|
||||
updateUnfilteredSectionCounts () {
|
||||
this.updateSectionCount(this.refs.communityThemesHeader, this.refs.communityCount, this.packages.user.length)
|
||||
this.updateSectionCount(this.refs.coreThemesHeader, this.refs.coreCount, this.packages.core.length)
|
||||
this.updateSectionCount(this.refs.developmentThemesHeader, this.refs.devCount, this.packages.dev.length)
|
||||
this.updateSectionCount(this.refs.gitThemesHeader, this.refs.gitCount, this.packages.git.length)
|
||||
|
||||
this.refs.totalPackages.textContent = `${this.packages.user.length + this.packages.core.length + this.packages.dev.length + this.packages.git.length}`
|
||||
}
|
||||
|
||||
updateFilteredSectionCounts () {
|
||||
const community = this.notHiddenCardsLength(this.refs.communityPackages)
|
||||
this.updateSectionCount(this.refs.communityThemesHeader, this.refs.communityCount, community, this.packages.user.length)
|
||||
|
||||
const dev = this.notHiddenCardsLength(this.refs.devPackages)
|
||||
this.updateSectionCount(this.refs.developmentThemesHeader, this.refs.devCount, dev, this.packages.dev.length)
|
||||
|
||||
const core = this.notHiddenCardsLength(this.refs.corePackages)
|
||||
this.updateSectionCount(this.refs.coreThemesHeader, this.refs.coreCount, core, this.packages.core.length)
|
||||
|
||||
const git = this.notHiddenCardsLength(this.refs.gitPackages)
|
||||
this.updateSectionCount(this.refs.gitThemesHeader, this.refs.gitCount, git, this.packages.git.length)
|
||||
|
||||
const shownThemes = dev + core + community + git
|
||||
const totalThemes = this.packages.user.length + this.packages.core.length + this.packages.dev.length + this.packages.git.length
|
||||
this.refs.totalPackages.textContent = `${shownThemes}/${totalThemes}`
|
||||
}
|
||||
|
||||
resetSectionHasItems () {
|
||||
this.resetCollapsibleSections([this.refs.communityThemesHeader, this.refs.coreThemesHeader, this.refs.developmentThemesHeader, this.refs.gitThemesHeader])
|
||||
}
|
||||
|
||||
matchPackages () {
|
||||
this.filterPackageListByText(this.refs.filterEditor.getText())
|
||||
}
|
||||
|
||||
didClickOpenUserStyleSheet (e) {
|
||||
e.preventDefault()
|
||||
atom.commands.dispatch(atom.views.getView(atom.workspace), 'application:open-your-stylesheet')
|
||||
}
|
||||
|
||||
didChangeUiMenu () {
|
||||
this.activeUiTheme = this.refs.uiMenu.value
|
||||
this.scheduleUpdateThemeConfig()
|
||||
}
|
||||
|
||||
didChangeSyntaxMenu () {
|
||||
this.activeSyntaxTheme = this.refs.syntaxMenu.value
|
||||
this.scheduleUpdateThemeConfig()
|
||||
}
|
||||
|
||||
didClickActiveUiThemeSettings (event) {
|
||||
event.stopPropagation()
|
||||
const theme = atom.themes.getActiveThemes().find((theme) => theme.metadata.theme === 'ui')
|
||||
const activeUiTheme = theme != null ? theme.metadata : null
|
||||
if (activeUiTheme != null) {
|
||||
this.settingsView.showPanel(this.activeUiTheme, {
|
||||
back: 'Themes',
|
||||
pack: activeUiTheme
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didClickActiveSyntaxThemeSettings (event) {
|
||||
event.stopPropagation()
|
||||
const theme = atom.themes.getActiveThemes().find((theme) => theme.metadata.theme === 'syntax')
|
||||
const activeSyntaxTheme = theme != null ? theme.metadata : null
|
||||
if (activeSyntaxTheme != null) {
|
||||
this.settingsView.showPanel(this.activeSyntaxTheme, {
|
||||
back: 'Themes',
|
||||
pack: activeSyntaxTheme
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
265
packages/settings-view/lib/updates-panel.js
Normal file
265
packages/settings-view/lib/updates-panel.js
Normal file
@ -0,0 +1,265 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable} from 'atom'
|
||||
import asyncQueue from 'async/queue'
|
||||
import etch from 'etch'
|
||||
|
||||
import ErrorView from './error-view'
|
||||
import PackageCard from './package-card'
|
||||
|
||||
export default class UpdatesPanel {
|
||||
constructor (settingsView, packageManager) {
|
||||
this.settingsView = settingsView
|
||||
this.packageManager = packageManager
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.updatingPackages = []
|
||||
this.packageCards = []
|
||||
|
||||
etch.initialize(this)
|
||||
|
||||
this.refs.updateAllButton.style.display = 'none'
|
||||
this.checkForUpdates()
|
||||
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}))
|
||||
|
||||
this.disposables.add(this.packageManager.on('package-updating theme-updating', ({pack, error}) => {
|
||||
this.refs.checkButton.disabled = true
|
||||
this.updatingPackages.push(pack)
|
||||
}))
|
||||
|
||||
this.disposables.add(
|
||||
this.packageManager.on('package-updated theme-updated package-update-failed theme-update-failed', ({pack, error}) => {
|
||||
if (error != null) {
|
||||
this.refs.updateErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.updatingPackages.length; i++) {
|
||||
const update = this.updatingPackages[i]
|
||||
if (update.name === pack.name) {
|
||||
this.updatingPackages.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.updatingPackages.length) {
|
||||
this.refs.checkButton.disabled = false
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.clearPackageCards()
|
||||
this.disposables.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div tabIndex='0' className='panels-item'>
|
||||
<section className='section packages'>
|
||||
<div className='section-container updates-container'>
|
||||
<div className='updates-heading-container'>
|
||||
<h1 className='section-heading icon icon-cloud-download'>Available Updates</h1>
|
||||
<div className='section-heading updates-btn-group'>
|
||||
<button
|
||||
ref='checkButton'
|
||||
className='update-all-button btn'
|
||||
onclick={() => { this.checkForUpdates(true) }}>Check for Updates</button>
|
||||
<button
|
||||
ref='updateAllButton'
|
||||
className='update-all-button btn btn-primary'
|
||||
onclick={() => { this.updateAll() }}>Update All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref='versionPinnedPackagesMessage' className='alert alert-warning icon icon-alert'>The following packages are pinned to their current version and are not being checked for updates: <strong>{ this.packageManager.getVersionPinnedPackages().join(', ') }</strong></div>
|
||||
<div ref='updateErrors' />
|
||||
<div ref='checkingMessage' className='alert alert-info icon icon-hourglass'>{`Checking for updates\u2026`}</div>
|
||||
<div ref='noUpdatesMessage' className='alert alert-info icon icon-heart'>All of your installed packages are up to date!</div>
|
||||
<div ref='updatesContainer' className='container package-container' />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
beforeShow (opts) {
|
||||
if (opts && opts.back) {
|
||||
this.refs.breadcrumb.textContent = opts.back
|
||||
this.refs.breadcrumb.onclick = () => { this.settingsView.showPanel(opts.back) }
|
||||
}
|
||||
|
||||
if (opts && opts.updates) {
|
||||
this.availableUpdates = opts.updates
|
||||
this.addUpdateViews()
|
||||
} else {
|
||||
this.availableUpdates = []
|
||||
this.clearPackageCards()
|
||||
this.checkForUpdates()
|
||||
}
|
||||
|
||||
if (this.packageManager.getVersionPinnedPackages().length === 0) {
|
||||
this.refs.versionPinnedPackagesMessage.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// Check for updates and display them
|
||||
async checkForUpdates (clearCache) {
|
||||
this.refs.noUpdatesMessage.style.display = 'none'
|
||||
this.refs.updateAllButton.disabled = true
|
||||
this.refs.checkButton.disabled = true
|
||||
this.refs.checkingMessage.style.display = ''
|
||||
|
||||
try {
|
||||
this.availableUpdates = await this.packageManager.getOutdated(clearCache)
|
||||
this.refs.checkButton.disabled = false
|
||||
this.addUpdateViews()
|
||||
} catch (error) {
|
||||
this.refs.checkButton.disabled = false
|
||||
this.refs.checkingMessage.style.display = 'none'
|
||||
this.refs.updateErrors.appendChild(new ErrorView(this.packageManager, error).element)
|
||||
}
|
||||
}
|
||||
|
||||
addUpdateViews () {
|
||||
if (this.availableUpdates.length > 0) {
|
||||
this.refs.updateAllButton.style.display = ''
|
||||
this.refs.updateAllButton.disabled = false
|
||||
}
|
||||
this.refs.checkingMessage.style.display = 'none'
|
||||
this.clearPackageCards()
|
||||
if (this.availableUpdates.length === 0) {
|
||||
this.refs.noUpdatesMessage.style.display = ''
|
||||
}
|
||||
|
||||
for (const pack of this.availableUpdates) {
|
||||
const packageCard = new PackageCard(pack, this.settingsView, this.packageManager, {back: 'Updates'})
|
||||
this.refs.updatesContainer.appendChild(packageCard.element)
|
||||
this.packageCards.push(packageCard)
|
||||
}
|
||||
}
|
||||
|
||||
async updateAll () {
|
||||
this.refs.checkButton.disabled = true
|
||||
this.refs.updateAllButton.disabled = true
|
||||
|
||||
let updatingPackages = this.updatingPackages
|
||||
let successfulUpdatesCount = 0
|
||||
let failedUpdatesCount = 0
|
||||
|
||||
const concurrency = atom.config.get('settings-view.packageUpdateConcurrency') > 0
|
||||
? atom.config.get('settings-view.packageUpdateConcurrency')
|
||||
: Number.POSITIVE_INFINITY
|
||||
|
||||
const queue = asyncQueue(function (packageCard, callback) {
|
||||
const onUpdateCompleted = function (err) {
|
||||
err == null ? successfulUpdatesCount++ : failedUpdatesCount++
|
||||
}
|
||||
|
||||
if (updatingPackages.includes(packageCard.pack)) {
|
||||
callback()
|
||||
} else {
|
||||
packageCard.update().then(onUpdateCompleted, onUpdateCompleted).then(callback)
|
||||
}
|
||||
}, concurrency)
|
||||
|
||||
queue.push(this.packageCards)
|
||||
|
||||
await queue.drain()
|
||||
|
||||
if (successfulUpdatesCount > 0) {
|
||||
const message = `Restart Atom to complete the update of ${successfulUpdatesCount} ${pluralize('package', successfulUpdatesCount)}:`
|
||||
let detail = ''
|
||||
this.packageCards.forEach((card) => {
|
||||
let oldVersion = ''
|
||||
let newVersion = ''
|
||||
|
||||
if (card.pack.apmInstallSource && card.pack.apmInstallSource.type === 'git') {
|
||||
oldVersion = card.pack.apmInstallSource.sha.substr(0, 8)
|
||||
newVersion = `${card.pack.latestSha.substr(0, 8)}`
|
||||
} else if (card.pack.version && card.pack.latestVersion) {
|
||||
oldVersion = card.pack.version
|
||||
newVersion = card.pack.latestVersion
|
||||
}
|
||||
|
||||
if (oldVersion && newVersion) {
|
||||
detail += `${card.pack.name}@${oldVersion} -> ${newVersion}\n`
|
||||
}
|
||||
})
|
||||
detail = detail.trim()
|
||||
|
||||
const notification = atom.notifications.addSuccess(message, {
|
||||
dismissable: true,
|
||||
buttons: [{
|
||||
text: 'Restart now',
|
||||
onDidClick () { return atom.restartApplication() }
|
||||
},
|
||||
{
|
||||
text: 'I\'ll do it later',
|
||||
onDidClick () { notification.dismiss() }
|
||||
}],
|
||||
detail
|
||||
})
|
||||
}
|
||||
|
||||
if (failedUpdatesCount === 0) {
|
||||
this.refs.checkButton.disabled = false
|
||||
this.refs.updateAllButton.style.display = 'none'
|
||||
} else {
|
||||
this.refs.checkButton.disabled = false
|
||||
this.refs.updateAllButton.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
clearPackageCards () {
|
||||
while (this.packageCards.length) {
|
||||
this.packageCards.pop().destroy()
|
||||
}
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
function pluralize (word, count) {
|
||||
return (count > 1) ? `${word}s` : word
|
||||
}
|
210
packages/settings-view/lib/uri-handler-panel.js
Normal file
210
packages/settings-view/lib/uri-handler-panel.js
Normal file
@ -0,0 +1,210 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import {CompositeDisposable} from 'atom'
|
||||
import etch from 'etch'
|
||||
|
||||
function isSupported () {
|
||||
return ['win32', 'darwin'].includes(process.platform)
|
||||
}
|
||||
|
||||
function isDefaultProtocolClient () {
|
||||
return require('electron').remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
|
||||
}
|
||||
|
||||
function setAsDefaultProtocolClient () {
|
||||
// This Electron API is only available on Windows and macOS. There might be some
|
||||
// hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440
|
||||
return isSupported() && require('electron').remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
|
||||
}
|
||||
|
||||
export default class UriHandlerPanel {
|
||||
constructor () {
|
||||
this.handleChange = this.handleChange.bind(this)
|
||||
this.handleBecomeProtocolClient = this.handleBecomeProtocolClient.bind(this)
|
||||
this.isDefaultProtocolClient = isDefaultProtocolClient()
|
||||
this.uriHistory = []
|
||||
etch.initialize(this)
|
||||
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions.add(
|
||||
atom.commands.add(this.element, {
|
||||
'core:move-up': () => { this.scrollUp() },
|
||||
'core:move-down': () => { this.scrollDown() },
|
||||
'core:page-up': () => { this.pageUp() },
|
||||
'core:page-down': () => { this.pageDown() },
|
||||
'core:move-to-top': () => { this.scrollToTop() },
|
||||
'core:move-to-bottom': () => { this.scrollToBottom() }
|
||||
}),
|
||||
atom.uriHandlerRegistry.onHistoryChange(() => {
|
||||
this.uriHistory = atom.uriHandlerRegistry.getRecentlyHandledURIs()
|
||||
etch.update(this)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
const schema = atom.config.getSchema('core.uriHandlerRegistration')
|
||||
|
||||
return (
|
||||
<div className='panels-item' tabIndex='0'>
|
||||
<form className='general-panel section'>
|
||||
<div className='settings-panel'>
|
||||
<div className='section-container'>
|
||||
<div className='block section-heading icon icon-device-desktop'>URI Handling</div>
|
||||
<div className='text icon icon-question'>These settings determine how Atom handles atom:// URIs.</div>
|
||||
<div className='section-body'>
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='setting-title'>URI Handler Registration</div>
|
||||
<div className='setting-description'>
|
||||
{this.renderRegistrationDescription()}
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
disabled={!isSupported() || this.isDefaultProtocolClient}
|
||||
style={{fontSize: '1.25em', display: 'block'}}
|
||||
onClick={this.handleBecomeProtocolClient}
|
||||
>
|
||||
Register as default atom:// protocol handler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='setting-title'>Default Registration</div>
|
||||
<div className='setting-description'>
|
||||
{schema.description}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
id='core.uriHandlerRegistration'
|
||||
className='form-control'
|
||||
onChange={this.handleChange}
|
||||
value={atom.config.get('core.uriHandlerRegistration')}
|
||||
>
|
||||
{schema.enum.map(({description, value}) => (
|
||||
<option value={value}>{description}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='controls-label'>
|
||||
<div className='setting-title'>Recent URIs</div>
|
||||
</label>
|
||||
<table className='uri-history'>
|
||||
<tr>
|
||||
<th>URI</th>
|
||||
<th>Handled By</th>
|
||||
</tr>
|
||||
{this.uriHistory.map(this.renderHistoryRow.bind(this))}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderHistoryRow (item, idx) {
|
||||
return (
|
||||
<tr
|
||||
key={item.id}
|
||||
className=''
|
||||
>
|
||||
<td>{item.uri}</td>
|
||||
<td>
|
||||
{item.handled
|
||||
? this.renderItem(item)
|
||||
: <em>not handled</em>
|
||||
}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
renderItem (item) {
|
||||
if (item.host === 'core') {
|
||||
return <em>core</em>
|
||||
} else {
|
||||
return <a href={`atom://config/packages/${item.host}`} onClick={this.handlePackageLinkClicked}>{item.host}</a>
|
||||
}
|
||||
}
|
||||
|
||||
handlePackageLinkClicked (evt) {
|
||||
evt.preventDefault()
|
||||
atom.workspace.open(evt.target.getAttribute('href'))
|
||||
}
|
||||
|
||||
renderRegistrationDescription () {
|
||||
if (this.isDefaultProtocolClient) {
|
||||
return 'Atom is already the default handler for atom:// URIs.'
|
||||
} else if (isSupported()) {
|
||||
return 'Register Atom as the default handler for atom:// URIs.'
|
||||
} else {
|
||||
return 'Registration as the default handler for atom:// URIs is only supported on Windows and macOS.'
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (evt) {
|
||||
atom.config.set('core.uriHandlerRegistration', evt.target.value)
|
||||
}
|
||||
|
||||
handleBecomeProtocolClient (evt) {
|
||||
evt.preventDefault()
|
||||
if (setAsDefaultProtocolClient()) {
|
||||
this.isDefaultProtocolClient = isDefaultProtocolClient()
|
||||
etch.update(this)
|
||||
} else {
|
||||
atom.notifications.addError('Could not become default protocol client')
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.element.focus()
|
||||
}
|
||||
|
||||
show () {
|
||||
this.element.style.display = ''
|
||||
}
|
||||
|
||||
scrollUp () {
|
||||
this.element.scrollTop -= document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
this.element.scrollTop += document.body.offsetHeight / 20
|
||||
}
|
||||
|
||||
pageUp () {
|
||||
this.element.scrollTop -= this.element.offsetHeight
|
||||
}
|
||||
|
||||
pageDown () {
|
||||
this.element.scrollTop += this.element.offsetHeight
|
||||
}
|
||||
|
||||
scrollToTop () {
|
||||
this.element.scrollTop = 0
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
}
|
40
packages/settings-view/lib/utils.js
Normal file
40
packages/settings-view/lib/utils.js
Normal file
@ -0,0 +1,40 @@
|
||||
const ownerFromRepository = repository => {
|
||||
if (!repository) return ''
|
||||
|
||||
const loginRegex = /github\.com\/([\w-]+)\/.+/
|
||||
let repo = repository
|
||||
if (typeof repository !== 'string') {
|
||||
repo = repository.url
|
||||
if (repo.match('git@github')) {
|
||||
const repoName = repo.split(':')[1]
|
||||
repo = `https://github.com/${repoName}`
|
||||
}
|
||||
}
|
||||
|
||||
if (!repo.match('github.com/')) {
|
||||
repo = `https://github.com/${repo}`
|
||||
}
|
||||
|
||||
const match = repo.match(loginRegex)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
const packageComparatorAscending = (left, right) => {
|
||||
const leftStatus = atom.packages.isPackageDisabled(left.name)
|
||||
const rightStatus = atom.packages.isPackageDisabled(right.name)
|
||||
if (leftStatus === rightStatus) {
|
||||
if (left.name > right.name) {
|
||||
return -1
|
||||
} else if (left.name < right.name) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
} else if (leftStatus > rightStatus) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {ownerFromRepository, packageComparatorAscending}
|
16
packages/settings-view/menus/settings-view.cson
Normal file
16
packages/settings-view/menus/settings-view.cson
Normal file
@ -0,0 +1,16 @@
|
||||
'menu': [
|
||||
{
|
||||
'label': 'Packages'
|
||||
'submenu': [
|
||||
'label': 'Settings View'
|
||||
'submenu': [
|
||||
{ 'label': 'Open', 'command': 'settings-view:open' }
|
||||
{ 'label': 'Show Keybindings', 'command': 'settings-view:show-keybindings' }
|
||||
{ 'label': 'Install Packages/Themes', 'command': 'settings-view:install-packages-and-themes' }
|
||||
{ 'label': 'Update Packages/Themes', 'command': 'settings-view:check-for-package-updates' }
|
||||
{ 'label': 'Manage Packages', 'command': 'settings-view:uninstall-packages' }
|
||||
{ 'label': 'Manage Themes', 'command': 'settings-view:uninstall-themes' }
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
1529
packages/settings-view/package-lock.json
generated
Normal file
1529
packages/settings-view/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
packages/settings-view/package.json
Normal file
52
packages/settings-view/package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "settings-view",
|
||||
"version": "0.261.11",
|
||||
"main": "./lib/main",
|
||||
"description": "Edit config settings, install packages, and change themes",
|
||||
"license": "MIT",
|
||||
"uriHandler": {
|
||||
"method": "handleURI",
|
||||
"deferActivation": false
|
||||
},
|
||||
"configSchema": {
|
||||
"packageUpdateConcurrency": {
|
||||
"title": "Maximum simultaneous package updates",
|
||||
"description": "Limit how many processes run simultaneously during package updates. If your machine slows down while updating many packages at once, set this value to a small positive number (e.g., `1` or `2`).",
|
||||
"type": "integer",
|
||||
"default": -1
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^3.2.0",
|
||||
"dompurify": "^2.0.17",
|
||||
"etch": "0.9.0",
|
||||
"fs-plus": "^3.0.0",
|
||||
"fuzzaldrin": "^2.1",
|
||||
"glob": "4.3.1",
|
||||
"hosted-git-info": "^2.8.9",
|
||||
"marked": "^4.0.10",
|
||||
"request": "^2.83.0",
|
||||
"season": "^6.0.2",
|
||||
"semver": "^5.3.0",
|
||||
"underscore-plus": "^1.0.6"
|
||||
},
|
||||
"repository": "https://github.com/pulsar-edit/pulsar",
|
||||
"engines": {
|
||||
"atom": "*"
|
||||
},
|
||||
"consumedServices": {
|
||||
"status-bar": {
|
||||
"versions": {
|
||||
"^1.0.0": "consumeStatusBar"
|
||||
}
|
||||
},
|
||||
"snippets": {
|
||||
"versions": {
|
||||
"0.1.0": "consumeSnippets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deserializers": {
|
||||
"SettingsView": "createSettingsView"
|
||||
}
|
||||
}
|
53
packages/settings-view/spec/atom-io-client-spec.coffee
Normal file
53
packages/settings-view/spec/atom-io-client-spec.coffee
Normal file
@ -0,0 +1,53 @@
|
||||
AtomIoClient = require '../lib/atom-io-client'
|
||||
|
||||
describe "AtomIoClient", ->
|
||||
|
||||
beforeEach ->
|
||||
@client = new AtomIoClient
|
||||
|
||||
it "fetches avatar from cache if the network is unavailable", ->
|
||||
spyOn(@client, 'online').andReturn(false)
|
||||
spyOn(@client, 'fetchAndCacheAvatar')
|
||||
expect(@client.fetchAndCacheAvatar).not.toHaveBeenCalled()
|
||||
@client.avatar 'test-user', ->
|
||||
|
||||
describe "request", ->
|
||||
it "fetches api json from cache if the network is unavailable", ->
|
||||
spyOn(@client, 'online').andReturn(false)
|
||||
spyOn(@client, 'fetchFromCache').andReturn({})
|
||||
spyOn(@client, 'request')
|
||||
@client.package 'test-package', ->
|
||||
|
||||
expect(@client.fetchFromCache).toHaveBeenCalled()
|
||||
expect(@client.request).not.toHaveBeenCalled()
|
||||
|
||||
it "returns an error if the API response is not JSON", ->
|
||||
jsonParse = JSON.parse
|
||||
|
||||
waitsFor (done) ->
|
||||
spyOn(@client, 'parseJSON').andThrow()
|
||||
@client.request 'path', (error, data) ->
|
||||
expect(error).not.toBeNull()
|
||||
done()
|
||||
|
||||
runs ->
|
||||
# Tests will throw without this as cleanup requires JSON.parse to work
|
||||
JSON.parse = jsonParse
|
||||
|
||||
it "handles glob errors", ->
|
||||
spyOn(@client, 'avatarGlob').andReturn "#{__dirname}/**"
|
||||
spyOn(require('fs'), 'readdir').andCallFake (dirPath, callback) ->
|
||||
process.nextTick -> callback(new Error('readdir error'))
|
||||
|
||||
callback = jasmine.createSpy('cacheAvatar callback')
|
||||
@client.cachedAvatar 'fakeperson', callback
|
||||
|
||||
waitsFor ->
|
||||
callback.callCount is 1
|
||||
|
||||
runs ->
|
||||
expect(callback.argsForCall[0][0].message).toBe 'readdir error'
|
||||
|
||||
xit "purges old items from cache correctly"
|
||||
# "correctly" in this case means "remove all old items but one" so that we
|
||||
# always have stale data to return if the network is gone.
|
110
packages/settings-view/spec/editor-panel-spec.coffee
Normal file
110
packages/settings-view/spec/editor-panel-spec.coffee
Normal file
@ -0,0 +1,110 @@
|
||||
EditorPanel = require '../lib/editor-panel'
|
||||
|
||||
describe "EditorPanel", ->
|
||||
panel = null
|
||||
|
||||
getValueForId = (id) ->
|
||||
element = panel.element.querySelector("##{id.replace(/\./g, '\\.')}")
|
||||
if element?.tagName is "INPUT"
|
||||
element.checked
|
||||
else if element?.tagName is "SELECT"
|
||||
element.value
|
||||
else if element?
|
||||
element.getModel().getText()
|
||||
else
|
||||
return
|
||||
|
||||
setValueForId = (id, value) ->
|
||||
element = panel.element.querySelector("##{id.replace(/\./g, '\\.')}")
|
||||
if element.tagName is "INPUT"
|
||||
element.checked = value
|
||||
element.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
else if element.tagName is "SELECT"
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
else
|
||||
element.getModel().setText(value?.toString())
|
||||
window.advanceClock(10000) # wait for contents-modified to be triggered
|
||||
|
||||
beforeEach ->
|
||||
atom.config.set('editor.boolean', true)
|
||||
atom.config.set('editor.string', 'hey')
|
||||
atom.config.set('editor.object', {boolean: true, int: 3, string: 'test'})
|
||||
atom.config.set('editor.simpleArray', ['a', 'b', 'c'])
|
||||
atom.config.set('editor.complexArray', ['a', 'b', {c: true}])
|
||||
|
||||
atom.config.setSchema('', type: 'object')
|
||||
|
||||
panel = new EditorPanel()
|
||||
|
||||
it "automatically binds named fields to their corresponding config keys", ->
|
||||
expect(getValueForId('editor.boolean')).toBeTruthy()
|
||||
expect(getValueForId('editor.string')).toBe 'hey'
|
||||
expect(getValueForId('editor.object.boolean')).toBeTruthy()
|
||||
expect(getValueForId('editor.object.int')).toBe '3'
|
||||
expect(getValueForId('editor.object.string')).toBe 'test'
|
||||
|
||||
atom.config.set('editor.boolean', false)
|
||||
atom.config.set('editor.string', 'hey again')
|
||||
atom.config.set('editor.object.boolean', false)
|
||||
atom.config.set('editor.object.int', 6)
|
||||
atom.config.set('editor.object.string', 'hi')
|
||||
|
||||
expect(getValueForId('editor.boolean')).toBeFalsy()
|
||||
expect(getValueForId('editor.string')).toBe 'hey again'
|
||||
expect(getValueForId('editor.object.boolean')).toBeFalsy()
|
||||
expect(getValueForId('editor.object.int')).toBe '6'
|
||||
expect(getValueForId('editor.object.string')).toBe 'hi'
|
||||
|
||||
setValueForId('editor.string', "oh hi")
|
||||
setValueForId('editor.boolean', true)
|
||||
setValueForId('editor.object.boolean', true)
|
||||
setValueForId('editor.object.int', 9)
|
||||
setValueForId('editor.object.string', 'yo')
|
||||
|
||||
expect(atom.config.get('editor.boolean')).toBe true
|
||||
expect(atom.config.get('editor.string')).toBe 'oh hi'
|
||||
expect(atom.config.get('editor.object.boolean')).toBe true
|
||||
expect(atom.config.get('editor.object.int')).toBe 9
|
||||
expect(atom.config.get('editor.object.string')).toBe 'yo'
|
||||
|
||||
setValueForId('editor.string', '')
|
||||
setValueForId('editor.object.int', '')
|
||||
setValueForId('editor.object.string', '')
|
||||
|
||||
expect(atom.config.get('editor.string')).toBeUndefined()
|
||||
expect(atom.config.get('editor.object.int')).toBeUndefined()
|
||||
expect(atom.config.get('editor.object.string')).toBeUndefined()
|
||||
|
||||
it "does not save the config value until it has been changed to a new value", ->
|
||||
observeHandler = jasmine.createSpy("observeHandler")
|
||||
atom.config.observe "editor.simpleArray", observeHandler
|
||||
observeHandler.reset()
|
||||
|
||||
window.advanceClock(10000) # wait for contents-modified to be triggered
|
||||
expect(observeHandler).not.toHaveBeenCalled()
|
||||
|
||||
setValueForId('editor.simpleArray', 2)
|
||||
expect(observeHandler).toHaveBeenCalled()
|
||||
observeHandler.reset()
|
||||
|
||||
setValueForId('editor.simpleArray', 2)
|
||||
expect(observeHandler).not.toHaveBeenCalled()
|
||||
|
||||
it "does not update the editor text unless the value it parses to changes", ->
|
||||
setValueForId('editor.simpleArray', "a, b,")
|
||||
expect(atom.config.get('editor.simpleArray')).toEqual ['a', 'b']
|
||||
expect(getValueForId('editor.simpleArray')).toBe 'a, b,'
|
||||
|
||||
it "only adds editors for arrays when all the values in the array are strings", ->
|
||||
expect(getValueForId('editor.simpleArray')).toBe 'a, b, c'
|
||||
expect(getValueForId('editor.complexArray')).toBeUndefined()
|
||||
|
||||
setValueForId('editor.simpleArray', 'a, d')
|
||||
|
||||
expect(atom.config.get('editor.simpleArray')).toEqual ['a', 'd']
|
||||
expect(atom.config.get('editor.complexArray')).toEqual ['a', 'b', {c: true}]
|
||||
|
||||
it "shows the package settings notes for core and editor settings", ->
|
||||
expect(panel.element.querySelector('#editor-settings-note')).toExist()
|
||||
expect(panel.element.querySelector('#editor-settings-note').textContent).toContain('Check language settings')
|
5
packages/settings-view/spec/fixtures/a-theme/package.json
vendored
Normal file
5
packages/settings-view/spec/fixtures/a-theme/package.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"theme": "syntax",
|
||||
"name": "a-theme",
|
||||
"version": "1.0.0"
|
||||
}
|
32
packages/settings-view/spec/fixtures/installed.json
vendored
Normal file
32
packages/settings-view/spec/fixtures/installed.json
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"core": [
|
||||
{
|
||||
"name": "core-theme",
|
||||
"theme": "syntax"
|
||||
},
|
||||
{
|
||||
"name": "core-package",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
],
|
||||
"user": [
|
||||
{
|
||||
"name": "user-theme",
|
||||
"theme": "syntax"
|
||||
},
|
||||
{
|
||||
"name": "user-package",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
],
|
||||
"dev": [
|
||||
{
|
||||
"name": "dev-theme",
|
||||
"theme": "syntax"
|
||||
},
|
||||
{
|
||||
"name": "dev-package",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
9
packages/settings-view/spec/fixtures/language-test/grammars/a.json
vendored
Normal file
9
packages/settings-view/spec/fixtures/language-test/grammars/a.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"scopeName": "source.a",
|
||||
"fileTypes": [
|
||||
".a",
|
||||
".aa",
|
||||
"a"
|
||||
],
|
||||
"name": "A Grammar"
|
||||
}
|
4
packages/settings-view/spec/fixtures/language-test/grammars/b.json
vendored
Normal file
4
packages/settings-view/spec/fixtures/language-test/grammars/b.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "B Grammar",
|
||||
"scopeName": "source.b"
|
||||
}
|
3
packages/settings-view/spec/fixtures/language-test/grammars/c.json
vendored
Normal file
3
packages/settings-view/spec/fixtures/language-test/grammars/c.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"scopeName": "source.c"
|
||||
}
|
4
packages/settings-view/spec/fixtures/language-test/keymaps/test.cson
vendored
Normal file
4
packages/settings-view/spec/fixtures/language-test/keymaps/test.cson
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
'.platform-n64':
|
||||
'cmd-g': 'language-test:run'
|
||||
'test':
|
||||
'cmd-g': 'language-test:run'
|
5
packages/settings-view/spec/fixtures/language-test/package.json
vendored
Normal file
5
packages/settings-view/spec/fixtures/language-test/package.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "language-test",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/language-test"
|
||||
}
|
8
packages/settings-view/spec/fixtures/language-test/snippets/bar.json
vendored
Normal file
8
packages/settings-view/spec/fixtures/language-test/snippets/bar.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
".source.b": {
|
||||
"BAR": {
|
||||
"prefix": "b",
|
||||
"body": "bar?\nline two"
|
||||
}
|
||||
}
|
||||
}
|
8
packages/settings-view/spec/fixtures/language-test/snippets/foo.json
vendored
Normal file
8
packages/settings-view/spec/fixtures/language-test/snippets/foo.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
".source.a": {
|
||||
"FOO": {
|
||||
"prefix": "f",
|
||||
"body": "foo!"
|
||||
}
|
||||
}
|
||||
}
|
0
packages/settings-view/spec/fixtures/package-internal/README.md
vendored
Normal file
0
packages/settings-view/spec/fixtures/package-internal/README.md
vendored
Normal file
4
packages/settings-view/spec/fixtures/package-internal/package.json
vendored
Normal file
4
packages/settings-view/spec/fixtures/package-internal/package.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "package-internal",
|
||||
"repository": "https://github.com/atom/atom"
|
||||
}
|
7
packages/settings-view/spec/fixtures/package-with-bugs-property-email/package.json
vendored
Normal file
7
packages/settings-view/spec/fixtures/package-with-bugs-property-email/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "package-with-bugs-property-email",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-with-bugs-property-email",
|
||||
"bugs": {"email": "issues@example.com"},
|
||||
"main": "main"
|
||||
}
|
7
packages/settings-view/spec/fixtures/package-with-bugs-property-url-string/package.json
vendored
Normal file
7
packages/settings-view/spec/fixtures/package-with-bugs-property-url-string/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "package-with-bugs-property-url-string",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-with-bugs-property-url-string",
|
||||
"bugs": "https://example.com/custom-issue-tracker/new",
|
||||
"main": "main"
|
||||
}
|
7
packages/settings-view/spec/fixtures/package-with-bugs-property-url/package.json
vendored
Normal file
7
packages/settings-view/spec/fixtures/package-with-bugs-property-url/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "package-with-bugs-property-url",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-with-bugs-property-url",
|
||||
"bugs": {"url": "https://example.com/custom-issue-tracker/new", "email": "issues@example.com"},
|
||||
"main": "main"
|
||||
}
|
5
packages/settings-view/spec/fixtures/package-with-config/main.coffee
vendored
Normal file
5
packages/settings-view/spec/fixtures/package-with-config/main.coffee
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports =
|
||||
config:
|
||||
setting:
|
||||
type: 'string'
|
||||
default: 'something'
|
6
packages/settings-view/spec/fixtures/package-with-config/package.json
vendored
Normal file
6
packages/settings-view/spec/fixtures/package-with-config/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-with-config",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-with-config",
|
||||
"main": "main"
|
||||
}
|
12
packages/settings-view/spec/fixtures/package-with-readme/README.md
vendored
Normal file
12
packages/settings-view/spec/fixtures/package-with-readme/README.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
I am a Readme!
|
||||
|
||||
* [ ] I'm a not completed task
|
||||
* [x] I'm completed
|
||||
|
||||
|
||||
![AbsoluteImage](https://example.com/static/image.jpg)
|
||||
![RelativeImage](static/image.jpg)
|
||||
![Base64Image](data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)
|
||||
|
||||
<script>alert('oh, hai');</script>
|
||||
<iframe src="https://pulsar-edit.dev"></iframe>
|
6
packages/settings-view/spec/fixtures/package-with-readme/package.json
vendored
Normal file
6
packages/settings-view/spec/fixtures/package-with-readme/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-with-readme",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-with-readme",
|
||||
"main": "main"
|
||||
}
|
6
packages/settings-view/spec/fixtures/package-without-bugs-property/package.json
vendored
Normal file
6
packages/settings-view/spec/fixtures/package-without-bugs-property/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-without-bugs-property",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/example/package-without-bugs-property",
|
||||
"main": "main"
|
||||
}
|
5
packages/settings-view/spec/fixtures/syntax-theme-with-config/main.coffee
vendored
Normal file
5
packages/settings-view/spec/fixtures/syntax-theme-with-config/main.coffee
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports =
|
||||
config:
|
||||
setting:
|
||||
type: 'string'
|
||||
default: 'something'
|
6
packages/settings-view/spec/fixtures/syntax-theme-with-config/package.json
vendored
Normal file
6
packages/settings-view/spec/fixtures/syntax-theme-with-config/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"theme": "syntax",
|
||||
"name": "syntax-theme-with-config",
|
||||
"version": "1.0.0",
|
||||
"main": "./main"
|
||||
}
|
5
packages/settings-view/spec/fixtures/ui-theme-with-config/main.coffee
vendored
Normal file
5
packages/settings-view/spec/fixtures/ui-theme-with-config/main.coffee
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports =
|
||||
config:
|
||||
setting:
|
||||
type: 'string'
|
||||
default: 'something'
|
6
packages/settings-view/spec/fixtures/ui-theme-with-config/package.json
vendored
Normal file
6
packages/settings-view/spec/fixtures/ui-theme-with-config/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"theme": "ui",
|
||||
"name": "ui-theme-with-config",
|
||||
"version": "1.0.0",
|
||||
"main": "./main"
|
||||
}
|
90
packages/settings-view/spec/general-panel-spec.coffee
Normal file
90
packages/settings-view/spec/general-panel-spec.coffee
Normal file
@ -0,0 +1,90 @@
|
||||
GeneralPanel = require '../lib/general-panel'
|
||||
|
||||
describe "GeneralPanel", ->
|
||||
panel = null
|
||||
|
||||
getValueForId = (id) ->
|
||||
element = panel.element.querySelector("##{id.replace(/\./g, '\\.')}")
|
||||
if element.tagName is "INPUT"
|
||||
element.checked
|
||||
else if element.tagName is "SELECT"
|
||||
element.value
|
||||
else
|
||||
element.getModel().getText()
|
||||
|
||||
setValueForId = (id, value) ->
|
||||
element = panel.element.querySelector("##{id.replace(/\./g, '\\.')}")
|
||||
if element.tagName is "INPUT"
|
||||
element.checked = value
|
||||
element.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
else if element.tagName is "SELECT"
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
else
|
||||
element.getModel().setText(value?.toString())
|
||||
window.advanceClock(10000) # wait for contents-modified to be triggered
|
||||
|
||||
beforeEach ->
|
||||
atom.config.set('core.enum', 4)
|
||||
atom.config.set('core.int', 22)
|
||||
atom.config.set('core.float', 0.1)
|
||||
|
||||
atom.config.setSchema('', type: 'object')
|
||||
atom.config.setSchema('core.enum',
|
||||
type: 'integer'
|
||||
default: 2
|
||||
enum: [2, 4, 6, 8]
|
||||
)
|
||||
|
||||
panel = new GeneralPanel()
|
||||
|
||||
it "automatically binds named fields to their corresponding config keys", ->
|
||||
expect(getValueForId('core.enum')).toBe '4'
|
||||
expect(getValueForId('core.int')).toBe '22'
|
||||
expect(getValueForId('core.float')).toBe '0.1'
|
||||
|
||||
atom.config.set('core.enum', 6)
|
||||
atom.config.set('core.int', 222)
|
||||
atom.config.set('core.float', 0.11)
|
||||
|
||||
expect(getValueForId('core.enum')).toBe '6'
|
||||
expect(getValueForId('core.int')).toBe '222'
|
||||
expect(getValueForId('core.float')).toBe '0.11'
|
||||
|
||||
setValueForId('core.enum', '2')
|
||||
setValueForId('core.int', 90)
|
||||
setValueForId('core.float', 89.2)
|
||||
|
||||
expect(atom.config.get('core.enum')).toBe 2
|
||||
expect(atom.config.get('core.int')).toBe 90
|
||||
expect(atom.config.get('core.float')).toBe 89.2
|
||||
|
||||
setValueForId('core.int', '')
|
||||
setValueForId('core.float', '')
|
||||
|
||||
expect(atom.config.get('core.int')).toBeUndefined()
|
||||
expect(atom.config.get('core.float')).toBeUndefined()
|
||||
|
||||
it "does not save the config value until it has been changed to a new value", ->
|
||||
observeHandler = jasmine.createSpy("observeHandler")
|
||||
atom.config.observe "core.int", observeHandler
|
||||
observeHandler.reset()
|
||||
|
||||
window.advanceClock(10000) # wait for contents-modified to be triggered
|
||||
expect(observeHandler).not.toHaveBeenCalled()
|
||||
|
||||
setValueForId('core.int', 2)
|
||||
expect(observeHandler).toHaveBeenCalled()
|
||||
observeHandler.reset()
|
||||
|
||||
setValueForId('core.int', 2)
|
||||
expect(observeHandler).not.toHaveBeenCalled()
|
||||
|
||||
it "does not update the editor text unless the value it parses to changes", ->
|
||||
setValueForId('core.int', "2.")
|
||||
expect(atom.config.get('core.int')).toBe 2
|
||||
expect(getValueForId('core.int')).toBe '2.'
|
||||
|
||||
it "shows the package settings notes for core and editor settings", ->
|
||||
expect(panel.element.querySelector('#core-settings-note')).toExist()
|
||||
expect(panel.element.querySelector('#core-settings-note').textContent).toContain('their package card in')
|
113
packages/settings-view/spec/install-panel-spec.coffee
Normal file
113
packages/settings-view/spec/install-panel-spec.coffee
Normal file
@ -0,0 +1,113 @@
|
||||
InstallPanel = require '../lib/install-panel'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
|
||||
describe 'InstallPanel', ->
|
||||
beforeEach ->
|
||||
settingsView = new SettingsView()
|
||||
@packageManager = new PackageManager()
|
||||
@panel = new InstallPanel(settingsView, @packageManager)
|
||||
|
||||
describe "when the packages button is clicked", ->
|
||||
beforeEach ->
|
||||
spyOn(@panel, 'search')
|
||||
@panel.refs.searchEditor.setText('something')
|
||||
|
||||
it "performs a search for the contents of the input", ->
|
||||
@panel.refs.searchPackagesButton.click()
|
||||
expect(@panel.searchType).toBe 'packages'
|
||||
expect(@panel.search).toHaveBeenCalledWith 'something'
|
||||
expect(@panel.search.callCount).toBe 1
|
||||
|
||||
@panel.refs.searchPackagesButton.click()
|
||||
expect(@panel.searchType).toBe 'packages'
|
||||
expect(@panel.search).toHaveBeenCalledWith 'something'
|
||||
expect(@panel.search.callCount).toBe 2
|
||||
|
||||
describe "when the themes button is clicked", ->
|
||||
beforeEach ->
|
||||
spyOn(@panel, 'search')
|
||||
@panel.refs.searchEditor.setText('something')
|
||||
|
||||
it "performs a search for the contents of the input", ->
|
||||
@panel.refs.searchThemesButton.click()
|
||||
expect(@panel.searchType).toBe 'themes'
|
||||
expect(@panel.search.callCount).toBe 1
|
||||
expect(@panel.search).toHaveBeenCalledWith 'something'
|
||||
|
||||
@panel.refs.searchThemesButton.click()
|
||||
expect(@panel.searchType).toBe 'themes'
|
||||
expect(@panel.search.callCount).toBe 2
|
||||
|
||||
describe "when the buttons are toggled", ->
|
||||
beforeEach ->
|
||||
spyOn(@panel, 'search')
|
||||
@panel.refs.searchEditor.setText('something')
|
||||
|
||||
it "performs a search for the contents of the input", ->
|
||||
@panel.refs.searchThemesButton.click()
|
||||
expect(@panel.searchType).toBe 'themes'
|
||||
expect(@panel.search.callCount).toBe 1
|
||||
expect(@panel.search).toHaveBeenCalledWith 'something'
|
||||
|
||||
@panel.refs.searchPackagesButton.click()
|
||||
expect(@panel.searchType).toBe 'packages'
|
||||
expect(@panel.search.callCount).toBe 2
|
||||
|
||||
@panel.refs.searchThemesButton.click()
|
||||
expect(@panel.searchType).toBe 'themes'
|
||||
expect(@panel.search.callCount).toBe 3
|
||||
|
||||
describe "searching packages", ->
|
||||
it "displays the packages in the order returned", ->
|
||||
spyOn(@panel.client, 'search').andCallFake -> Promise.resolve([{name: 'not-first'}, {name: 'first'}])
|
||||
spyOn(@panel, 'getPackageCardView').andCallThrough()
|
||||
|
||||
waitsForPromise =>
|
||||
@panel.search('first')
|
||||
|
||||
runs ->
|
||||
expect(@panel.getPackageCardView.argsForCall[0][0].name).toEqual 'not-first'
|
||||
expect(@panel.getPackageCardView.argsForCall[1][0].name).toEqual 'first'
|
||||
|
||||
describe "searching git packages", ->
|
||||
beforeEach ->
|
||||
spyOn(@panel, 'showGitInstallPackageCard').andCallThrough()
|
||||
|
||||
it "shows a git installation card with git specific info for ssh URLs", ->
|
||||
query = 'git@github.com:user/repo.git'
|
||||
@panel.performSearchForQuery(query)
|
||||
args = @panel.showGitInstallPackageCard.argsForCall[0][0]
|
||||
expect(args.name).toEqual query
|
||||
expect(args.gitUrlInfo).toBeTruthy()
|
||||
|
||||
it "shows a git installation card with git specific info for https URLs", ->
|
||||
query = 'https://github.com/user/repo.git'
|
||||
@panel.performSearchForQuery(query)
|
||||
args = @panel.showGitInstallPackageCard.argsForCall[0][0]
|
||||
expect(args.name).toEqual query
|
||||
expect(args.gitUrlInfo).toBeTruthy()
|
||||
|
||||
it "shows a git installation card with git specific info for shortcut URLs", ->
|
||||
query = 'user/repo'
|
||||
@panel.performSearchForQuery(query)
|
||||
args = @panel.showGitInstallPackageCard.argsForCall[0][0]
|
||||
expect(args.name).toEqual query
|
||||
expect(args.gitUrlInfo).toBeTruthy()
|
||||
|
||||
it "doesn't show a git installation card for normal packages", ->
|
||||
query = 'this-package-is-so-normal'
|
||||
@panel.performSearchForQuery(query)
|
||||
expect(@panel.showGitInstallPackageCard).not.toHaveBeenCalled()
|
||||
|
||||
describe "when a package with the same gitUrlInfo property is installed", ->
|
||||
beforeEach ->
|
||||
@gitUrlInfo = jasmine.createSpy('gitUrlInfo')
|
||||
@panel.showGitInstallPackageCard(gitUrlInfo: @gitUrlInfo)
|
||||
|
||||
it "replaces the package card with the newly installed pack object", ->
|
||||
newPack =
|
||||
gitUrlInfo: @gitUrlInfo
|
||||
spyOn(@panel, 'updateGitPackageCard')
|
||||
@packageManager.emitter.emit('package-installed', {pack: newPack})
|
||||
expect(@panel.updateGitPackageCard).toHaveBeenCalledWith newPack
|
338
packages/settings-view/spec/installed-package-view-spec.coffee
Normal file
338
packages/settings-view/spec/installed-package-view-spec.coffee
Normal file
@ -0,0 +1,338 @@
|
||||
path = require 'path'
|
||||
PackageDetailView = require '../lib/package-detail-view'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
PackageKeymapView = require '../lib/package-keymap-view'
|
||||
PackageSnippetsView = require '../lib/package-snippets-view'
|
||||
_ = require 'underscore-plus'
|
||||
SnippetsProvider =
|
||||
getSnippets: -> atom.config.scopedSettingsStore.propertySets
|
||||
|
||||
describe "InstalledPackageView", ->
|
||||
beforeEach ->
|
||||
spyOn(PackageManager.prototype, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
|
||||
it "displays the grammars registered by the package", ->
|
||||
settingsPanels = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
settingsPanels = view.element.querySelectorAll('.package-grammars .settings-panel')
|
||||
|
||||
waitsFor ->
|
||||
children = Array.from(settingsPanels).map((s) -> s.children.length)
|
||||
childrenCount = children.reduce(((a, b) -> a + b), 0)
|
||||
childrenCount is 2
|
||||
|
||||
expect(settingsPanels[0].querySelector('.grammar-scope').textContent).toBe 'Scope: source.a'
|
||||
expect(settingsPanels[0].querySelector('.grammar-filetypes').textContent).toBe 'File Types: .a, .aa, a'
|
||||
|
||||
expect(settingsPanels[1].querySelector('.grammar-scope').textContent).toBe 'Scope: source.b'
|
||||
expect(settingsPanels[1].querySelector('.grammar-filetypes').textContent).toBe 'File Types: '
|
||||
|
||||
expect(settingsPanels[2]).toBeUndefined()
|
||||
|
||||
it "displays the snippets registered by the package", ->
|
||||
snippetsTable = null
|
||||
snippetsModule = null
|
||||
|
||||
# Relies on behavior not present in the snippets package before 1.33.
|
||||
# TODO: These tests should always run once 1.33 is released.
|
||||
shouldRunScopeTest = parseFloat(atom.getVersion()) >= 1.33
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('snippets').then (p) ->
|
||||
snippetsModule = p.mainModule
|
||||
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
|
||||
|
||||
SnippetsProvider =
|
||||
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
|
||||
|
||||
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
snippetsTable = view.element.querySelector('.package-snippets-table tbody')
|
||||
|
||||
waitsFor 'snippets table children to contain 2 items', ->
|
||||
snippetsTable.children.length >= 2
|
||||
|
||||
runs ->
|
||||
expect(snippetsTable.querySelector('tr:nth-child(1) td:nth-child(1)').textContent).toBe 'b'
|
||||
expect(snippetsTable.querySelector('tr:nth-child(1) td:nth-child(2)').textContent).toBe 'BAR'
|
||||
expect(snippetsTable.querySelector('tr:nth-child(1) td.snippet-scope-name').textContent).toBe '.b.source' if shouldRunScopeTest
|
||||
|
||||
expect(snippetsTable.querySelector('tr:nth-child(2) td:nth-child(1)').textContent).toBe 'f'
|
||||
expect(snippetsTable.querySelector('tr:nth-child(2) td:nth-child(2)').textContent).toBe 'FOO'
|
||||
expect(snippetsTable.querySelector('tr:nth-child(2) td.snippet-scope-name').textContent).toBe '.a.source' if shouldRunScopeTest
|
||||
|
||||
describe "when a snippet body is viewed", ->
|
||||
it "shows a tooltip", ->
|
||||
tooltipCalls = []
|
||||
view = null
|
||||
snippetsTable = null
|
||||
snippetsModule = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('snippets').then (p) ->
|
||||
snippetsModule = p.mainModule
|
||||
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
|
||||
|
||||
SnippetsProvider =
|
||||
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
|
||||
|
||||
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
snippetsTable = view.element.querySelector('.package-snippets-table tbody')
|
||||
|
||||
waitsFor 'snippets table children to contain 2 items', ->
|
||||
snippetsTable.children.length >= 2
|
||||
|
||||
runs ->
|
||||
expect(view.element.ownerDocument.querySelector('.snippet-body-tooltip')).not.toExist()
|
||||
|
||||
view.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-view-btn').click()
|
||||
expect(view.element.ownerDocument.querySelector('.snippet-body-tooltip')).toExist()
|
||||
|
||||
|
||||
# Relies on behavior not present in the snippets package before 1.33.
|
||||
# TODO: These tests should always run once 1.33 is released.
|
||||
if parseFloat(atom.getVersion()) >= 1.33
|
||||
describe "when a snippet is copied", ->
|
||||
[pack, card] = []
|
||||
snippetsTable = null
|
||||
snippetsModule = null
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('snippets').then (p) ->
|
||||
snippetsModule = p.mainModule
|
||||
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
|
||||
|
||||
SnippetsProvider =
|
||||
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
|
||||
getUserSnippetsPath: snippetsModule.getUserSnippetsPath()
|
||||
|
||||
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
card = new PackageSnippetsView(pack, SnippetsProvider)
|
||||
snippetsTable = card.element.querySelector('.package-snippets-table tbody')
|
||||
|
||||
waitsFor 'snippets table children to contain 2 items', ->
|
||||
snippetsTable.children.length >= 2
|
||||
|
||||
describe "when the snippets file ends in .cson", ->
|
||||
it "writes a CSON snippet to the clipboard", ->
|
||||
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.cson')
|
||||
card.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-copy-btn').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
\n'.b.source':
|
||||
'BAR':
|
||||
'prefix': 'b'
|
||||
'body': 'bar?\\nline two'\n
|
||||
"""
|
||||
|
||||
describe "when the snippets file ends in .json", ->
|
||||
it "writes a JSON snippet to the clipboard", ->
|
||||
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.json')
|
||||
card.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .btn:nth-child(2)').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
\n ".b.source": {
|
||||
"BAR": {
|
||||
"prefix": "b",
|
||||
"body": "bar?\\nline two"
|
||||
}
|
||||
}\n
|
||||
"""
|
||||
|
||||
describe "when the snippets toggle is clicked", ->
|
||||
it "sets the packagesWithSnippetsDisabled config to include the package name", ->
|
||||
[pack, card] = []
|
||||
snippetsModule = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('snippets').then (p) ->
|
||||
snippetsModule = p.mainModule
|
||||
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
|
||||
|
||||
SnippetsProvider =
|
||||
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
|
||||
|
||||
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
card = new PackageSnippetsView(pack, SnippetsProvider)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
card.refs.snippetToggle.click()
|
||||
expect(card.refs.snippetToggle.checked).toBe false
|
||||
expect(_.include(atom.config.get('core.packagesWithSnippetsDisabled') ? [], 'language-test')).toBe true
|
||||
|
||||
waitsFor 'snippets table to update', ->
|
||||
card.refs.snippets.classList.contains('text-subtle')
|
||||
|
||||
runs ->
|
||||
card.refs.snippetToggle.click()
|
||||
expect(card.refs.snippetToggle.checked).toBe true
|
||||
expect(_.include(atom.config.get('core.packagesWithSnippetsDisabled') ? [], 'language-test')).toBe false
|
||||
|
||||
waitsFor 'snippets table to update', ->
|
||||
not card.refs.snippets.classList.contains('text-subtle')
|
||||
|
||||
it "does not display keybindings from other platforms", ->
|
||||
keybindingsTable = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
keybindingsTable = view.element.querySelector('.package-keymap-table tbody')
|
||||
expect(keybindingsTable.children.length).toBe 1
|
||||
|
||||
describe "when the keybindings toggle is clicked", ->
|
||||
it "sets the packagesWithKeymapsDisabled config to include the package name", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
card = new PackageKeymapView(pack)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
card.refs.keybindingToggle.click()
|
||||
expect(card.refs.keybindingToggle.checked).toBe false
|
||||
expect(_.include(atom.config.get('core.packagesWithKeymapsDisabled') ? [], 'language-test')).toBe true
|
||||
|
||||
if atom.keymaps.build?
|
||||
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody.text-subtle tr')
|
||||
expect(keybindingRows.length).toBe 1
|
||||
|
||||
card.refs.keybindingToggle.click()
|
||||
expect(card.refs.keybindingToggle.checked).toBe true
|
||||
expect(_.include(atom.config.get('core.packagesWithKeymapsDisabled') ? [], 'language-test')).toBe false
|
||||
|
||||
if atom.keymaps.build?
|
||||
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody tr')
|
||||
expect(keybindingRows.length).toBe 1
|
||||
|
||||
describe "when a keybinding is copied", ->
|
||||
[pack, card] = []
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getActivePackage('language-test')
|
||||
card = new PackageKeymapView(pack)
|
||||
|
||||
describe "when the keybinding file ends in .cson", ->
|
||||
it "writes a CSON snippet to the clipboard", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.cson'
|
||||
card.element.querySelector('.copy-icon').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
'test':
|
||||
'cmd-g': 'language-test:run'
|
||||
"""
|
||||
|
||||
describe "when the keybinding file ends in .json", ->
|
||||
it "writes a JSON snippet to the clipboard", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.json'
|
||||
card.element.querySelector('.copy-icon').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
"test": {
|
||||
"cmd-g": "language-test:run"
|
||||
}
|
||||
"""
|
||||
|
||||
describe "when the package is active", ->
|
||||
it "displays the correct enablement state", ->
|
||||
packageCard = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('status-bar')
|
||||
|
||||
runs ->
|
||||
expect(atom.packages.isPackageActive('status-bar')).toBe(true)
|
||||
pack = atom.packages.getLoadedPackage('status-bar')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
packageCard = view.element.querySelector('.package-card')
|
||||
|
||||
runs ->
|
||||
# Trigger observeDisabledPackages() here
|
||||
# because it is not default in specs
|
||||
atom.packages.observeDisabledPackages()
|
||||
atom.packages.disablePackage('status-bar')
|
||||
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true)
|
||||
expect(packageCard.classList.contains('disabled')).toBe(true)
|
||||
|
||||
describe "when the package is not active", ->
|
||||
it "displays the correct enablement state", ->
|
||||
atom.packages.loadPackage('status-bar')
|
||||
expect(atom.packages.isPackageActive('status-bar')).toBe(false)
|
||||
pack = atom.packages.getLoadedPackage('status-bar')
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
packageCard = view.element.querySelector('.package-card')
|
||||
|
||||
# Trigger observeDisabledPackages() here
|
||||
# because it is not default in specs
|
||||
atom.packages.observeDisabledPackages()
|
||||
atom.packages.disablePackage('status-bar')
|
||||
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true)
|
||||
expect(packageCard.classList.contains('disabled')).toBe(true)
|
||||
|
||||
it "still loads the config schema for the package", ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
waitsFor ->
|
||||
atom.packages.isPackageLoaded('package-with-config') is true
|
||||
|
||||
runs ->
|
||||
expect(atom.config.get('package-with-config.setting')).toBe undefined
|
||||
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
|
||||
expect(atom.config.get('package-with-config.setting')).toBe 'something'
|
||||
|
||||
describe "when the package was not installed from atom.io", ->
|
||||
normalizePackageDataReadmeError = 'ERROR: No README data found!'
|
||||
|
||||
it "still displays the Readme", ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
|
||||
|
||||
waitsFor ->
|
||||
atom.packages.isPackageLoaded('package-with-readme') is true
|
||||
|
||||
runs ->
|
||||
pack = atom.packages.getLoadedPackage('package-with-readme')
|
||||
expect(pack.metadata.readme).toBe normalizePackageDataReadmeError
|
||||
|
||||
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
|
||||
expect(view.refs.sections.querySelector('.package-readme').textContent).not.toBe normalizePackageDataReadmeError
|
||||
expect(view.refs.sections.querySelector('.package-readme').textContent.trim()).toContain 'I am a Readme!'
|
245
packages/settings-view/spec/installed-packages-panel-spec.coffee
Normal file
245
packages/settings-view/spec/installed-packages-panel-spec.coffee
Normal file
@ -0,0 +1,245 @@
|
||||
path = require 'path'
|
||||
|
||||
fs = require 'fs-plus'
|
||||
InstalledPackagesPanel = require '../lib/installed-packages-panel'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
PackageCard = require '../lib/package-card'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
|
||||
describe 'InstalledPackagesPanel', ->
|
||||
describe 'when the packages are loading', ->
|
||||
it 'filters packages by name once they have loaded', ->
|
||||
settingsView = new SettingsView
|
||||
@packageManager = new PackageManager
|
||||
@installed = JSON.parse fs.readFileSync(path.join(__dirname, 'fixtures', 'installed.json'))
|
||||
spyOn(@packageManager, 'getOutdated').andReturn new Promise ->
|
||||
spyOn(@packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(@packageManager, 'getInstalled').andReturn Promise.resolve(@installed)
|
||||
@panel = new InstalledPackagesPanel(settingsView, @packageManager)
|
||||
@panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(@panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
|
||||
waitsFor ->
|
||||
@packageManager.getInstalled.callCount is 1 and @panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
runs ->
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '1/1'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(@panel.refs.coreCount.textContent.trim()).toBe '0/1'
|
||||
expect(@panel.refs.corePackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
expect(@panel.refs.devCount.textContent.trim()).toBe '0/1'
|
||||
expect(@panel.refs.devPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
expect(@panel.refs.deprecatedCount.textContent.trim()).toBe '0/0'
|
||||
expect(@panel.refs.deprecatedPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
describe 'when the packages have finished loading', ->
|
||||
beforeEach ->
|
||||
settingsView = new SettingsView
|
||||
@packageManager = new PackageManager
|
||||
@installed = JSON.parse fs.readFileSync(path.join(__dirname, 'fixtures', 'installed.json'))
|
||||
spyOn(@packageManager, 'getOutdated').andReturn new Promise ->
|
||||
spyOn(@packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(@packageManager, 'getInstalled').andReturn Promise.resolve(@installed)
|
||||
@panel = new InstalledPackagesPanel(settingsView, @packageManager)
|
||||
|
||||
waitsFor ->
|
||||
@packageManager.getInstalled.callCount is 1 and @panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
it 'shows packages', ->
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(@panel.refs.coreCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.corePackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(@panel.refs.devCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.devPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(@panel.refs.deprecatedCount.textContent.trim()).toBe '0'
|
||||
expect(@panel.refs.deprecatedPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
it 'filters packages by name', ->
|
||||
@panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(@panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '1/1'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(@panel.refs.coreCount.textContent.trim()).toBe '0/1'
|
||||
expect(@panel.refs.corePackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
expect(@panel.refs.devCount.textContent.trim()).toBe '0/1'
|
||||
expect(@panel.refs.devPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
expect(@panel.refs.deprecatedCount.textContent.trim()).toBe '0/0'
|
||||
expect(@panel.refs.deprecatedPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
it 'adds newly installed packages to the list', ->
|
||||
[installCallback] = []
|
||||
spyOn(@packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
installCallback = callback
|
||||
onWillThrowError: ->
|
||||
spyOn(atom.packages, 'activatePackage').andCallFake (name) =>
|
||||
@installed.user.push {name}
|
||||
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
@packageManager.install({name: 'another-user-package'})
|
||||
installCallback(0, '', '')
|
||||
|
||||
advanceClock InstalledPackagesPanel.loadPackagesDelay()
|
||||
waits 1
|
||||
runs ->
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '2'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 2
|
||||
|
||||
it 'removes uninstalled packages from the list', ->
|
||||
[uninstallCallback] = []
|
||||
spyOn(@packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
uninstallCallback = callback
|
||||
onWillThrowError: ->
|
||||
spyOn(@packageManager, 'unload').andCallFake (name) =>
|
||||
@installed.user = []
|
||||
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
@packageManager.uninstall({name: 'user-package'})
|
||||
uninstallCallback(0, '', '')
|
||||
|
||||
advanceClock InstalledPackagesPanel.loadPackagesDelay()
|
||||
waits 1
|
||||
runs ->
|
||||
expect(@panel.refs.communityCount.textContent.trim()).toBe '0'
|
||||
expect(@panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
it 'correctly handles deprecated packages', ->
|
||||
resolve = null
|
||||
promise = new Promise (r) -> resolve = r
|
||||
jasmine.unspy(@packageManager, 'getOutdated')
|
||||
spyOn(@packageManager, 'getOutdated').andReturn(promise)
|
||||
jasmine.attachToDOM(@panel.element)
|
||||
|
||||
[updateCallback] = []
|
||||
spyOn(atom.packages, 'isDeprecatedPackage').andCallFake =>
|
||||
return true if @installed.user[0].version is '1.0.0'
|
||||
false
|
||||
spyOn(@packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
updateCallback = callback
|
||||
onWillThrowError: ->
|
||||
atom.packages.activatePackage
|
||||
spyOn(atom.packages, 'activatePackage').andCallFake (name) =>
|
||||
@installed.user[0].version = '1.1.0'
|
||||
|
||||
expect(@panel.refs.deprecatedSection).not.toBeVisible()
|
||||
@panel.loadPackages()
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(@panel.refs.deprecatedSection).toBeVisible()
|
||||
expect(@panel.refs.deprecatedCount.textContent.trim()).toBe '1'
|
||||
expect(@panel.refs.deprecatedPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
spyOn(PackageCard::, 'displayAvailableUpdate')
|
||||
resolve([{name: 'user-package', latestVersion: '1.1.0'}])
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(PackageCard::displayAvailableUpdate).toHaveBeenCalledWith('1.1.0')
|
||||
|
||||
describe 'expanding and collapsing sub-sections', ->
|
||||
beforeEach ->
|
||||
settingsView = new SettingsView
|
||||
@packageManager = new PackageManager
|
||||
@installed = JSON.parse fs.readFileSync(path.join(__dirname, 'fixtures', 'installed.json'))
|
||||
spyOn(@packageManager, 'getOutdated').andReturn new Promise ->
|
||||
spyOn(@packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(@packageManager, 'getInstalled').andReturn Promise.resolve(@installed)
|
||||
spyOn(atom.packages, 'isDeprecatedPackage').andCallFake =>
|
||||
return true if @installed.user[0].version is '1.0.0'
|
||||
false
|
||||
|
||||
@panel = new InstalledPackagesPanel(settingsView, @packageManager)
|
||||
|
||||
waitsFor ->
|
||||
@packageManager.getInstalled.callCount is 1 and @panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
it 'collapses and expands a sub-section if its header is clicked', ->
|
||||
@panel.element.querySelector('.sub-section.installed-packages .sub-section-heading').click()
|
||||
expect(@panel.element.querySelector('.sub-section.installed-packages')).toHaveClass 'collapsed'
|
||||
|
||||
expect(@panel.element.querySelector('.sub-section.deprecated-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
@panel.element.querySelector('.sub-section.installed-packages .sub-section-heading').click()
|
||||
expect(@panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'can collapse and expand any of the sub-sections', ->
|
||||
expect(@panel.element.querySelectorAll('.sub-section-heading.has-items').length).toBe 4
|
||||
|
||||
for element in @panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
element.click()
|
||||
|
||||
expect(@panel.element.querySelector('.sub-section.deprecated-packages')).toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.installed-packages')).toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.core-packages')).toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.dev-packages')).toHaveClass 'collapsed'
|
||||
|
||||
for element in @panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
element.click()
|
||||
|
||||
expect(@panel.element.querySelector('.sub-section.deprecated-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'can collapse sub-sections when filtering', ->
|
||||
@panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(@panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
|
||||
hasItems = @panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
expect(hasItems.length).toBe 2
|
||||
expect(hasItems[0].textContent).toMatch /Deprecated Packages/
|
||||
expect(hasItems[1].textContent).toMatch /Community Packages/
|
||||
|
||||
describe 'when there are no packages', ->
|
||||
beforeEach ->
|
||||
settingsView = new SettingsView
|
||||
@packageManager = new PackageManager
|
||||
@installed =
|
||||
dev: []
|
||||
user: []
|
||||
core: []
|
||||
spyOn(@packageManager, 'getOutdated').andReturn new Promise ->
|
||||
spyOn(@packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(@packageManager, 'getInstalled').andReturn Promise.resolve(@installed)
|
||||
@panel = new InstalledPackagesPanel(settingsView, @packageManager)
|
||||
|
||||
waitsFor ->
|
||||
@packageManager.getInstalled.callCount is 1 and @panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
it 'has a count of zero in all headings', ->
|
||||
expect(@panel.element.querySelector('.section-heading-count').textContent).toMatch /^0+$/
|
||||
expect(@panel.element.querySelectorAll('.sub-section .icon-package').length).toBe 5
|
||||
expect(@panel.element.querySelectorAll('.sub-section .icon-package.has-items').length).toBe 0
|
||||
|
||||
it 'can not collapse and expand any of the sub-sections', ->
|
||||
for element in @panel.element.querySelectorAll('.sub-section .icon-package')
|
||||
element.click()
|
||||
|
||||
expect(@panel.element.querySelector('.sub-section.deprecated-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(@panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'does not allow collapsing on any section when filtering', ->
|
||||
@panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(@panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
|
||||
expect(@panel.element.querySelector('.section-heading-count').textContent).toMatch /^(0\/0)+$/
|
||||
expect(@panel.element.querySelectorAll('.sub-section .icon-package').length).toBe 5
|
||||
expect(@panel.element.querySelectorAll('.sub-section .icon-paintcan.has-items').length).toBe 0
|
132
packages/settings-view/spec/keybindings-panel-spec.coffee
Normal file
132
packages/settings-view/spec/keybindings-panel-spec.coffee
Normal file
@ -0,0 +1,132 @@
|
||||
path = require 'path'
|
||||
KeybindingsPanel = require '../lib/keybindings-panel'
|
||||
|
||||
describe "KeybindingsPanel", ->
|
||||
[keyBindings, panel] = []
|
||||
|
||||
beforeEach ->
|
||||
expect(atom.keymaps).toBeDefined()
|
||||
keySource = "#{atom.getLoadSettings().resourcePath}#{path.sep}keymaps"
|
||||
keyBindings = [
|
||||
{
|
||||
source: keySource
|
||||
keystrokes: 'ctrl-a'
|
||||
command: 'core:select-all'
|
||||
selector: '.editor, .platform-test'
|
||||
}
|
||||
{
|
||||
source: keySource
|
||||
keystrokes: 'ctrl-u'
|
||||
command: 'core:undo'
|
||||
selector: ".platform-test"
|
||||
}
|
||||
{
|
||||
source: keySource
|
||||
keystrokes: 'ctrl-u'
|
||||
command: 'core:undo'
|
||||
selector: ".platform-a, .platform-b"
|
||||
}
|
||||
{
|
||||
source: keySource
|
||||
keystrokes: 'shift-\\ \\'
|
||||
command: 'core:undo'
|
||||
selector: '.editor'
|
||||
}
|
||||
{
|
||||
source: keySource
|
||||
keystrokes: 'ctrl-z\''
|
||||
command: 'core:toggle'
|
||||
selector: 'atom-text-editor[data-grammar~=\'css\']'
|
||||
}
|
||||
]
|
||||
spyOn(atom.keymaps, 'getKeyBindings').andReturn(keyBindings)
|
||||
panel = new KeybindingsPanel
|
||||
|
||||
it "loads and displays core key bindings", ->
|
||||
expect(panel.refs.keybindingRows.children.length).toBe 3
|
||||
|
||||
row = panel.refs.keybindingRows.children[0]
|
||||
expect(row.querySelector('.keystroke').textContent).toBe 'ctrl-a'
|
||||
expect(row.querySelector('.command').textContent).toBe 'core:select-all'
|
||||
expect(row.querySelector('.source').textContent).toBe 'Core'
|
||||
expect(row.querySelector('.selector').textContent).toBe '.editor, .platform-test'
|
||||
|
||||
describe "when a keybinding is copied", ->
|
||||
describe "when the keybinding file ends in .cson", ->
|
||||
it "writes a CSON snippet to the clipboard", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.cson'
|
||||
panel.element.querySelector('.copy-icon').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
'.editor, .platform-test':
|
||||
'ctrl-a': 'core:select-all'
|
||||
"""
|
||||
|
||||
describe "when the keybinding file ends in .json", ->
|
||||
it "writes a JSON snippet to the clipboard", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.json'
|
||||
panel.element.querySelector('.copy-icon').click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
".editor, .platform-test": {
|
||||
"ctrl-a": "core:select-all"
|
||||
}
|
||||
"""
|
||||
|
||||
describe "when the keybinding contains special characters", ->
|
||||
it "escapes the backslashes before copying", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.cson'
|
||||
panel.element.querySelectorAll('.copy-icon')[2].click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
'.editor':
|
||||
'shift-\\\\ \\\\': 'core:undo'
|
||||
"""
|
||||
|
||||
it "escapes the single quotes before copying", ->
|
||||
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.cson'
|
||||
panel.element.querySelectorAll('.copy-icon')[1].click()
|
||||
expect(atom.clipboard.read()).toBe """
|
||||
'atom-text-editor[data-grammar~=\\'css\\']':
|
||||
'ctrl-z\\'': 'core:toggle'
|
||||
"""
|
||||
|
||||
describe "when the key bindings change", ->
|
||||
it "reloads the key bindings", ->
|
||||
keyBindings.push
|
||||
source: atom.keymaps.getUserKeymapPath(), keystrokes: 'ctrl-b', command: 'core:undo', selector: '.editor'
|
||||
atom.keymaps.emitter.emit 'did-reload-keymap'
|
||||
|
||||
waitsFor "the new keybinding to show up in the keybinding panel", ->
|
||||
panel.refs.keybindingRows.children.length is 4
|
||||
|
||||
runs ->
|
||||
row = panel.refs.keybindingRows.children[1]
|
||||
expect(row.querySelector('.keystroke').textContent).toBe 'ctrl-b'
|
||||
expect(row.querySelector('.command').textContent).toBe 'core:undo'
|
||||
expect(row.querySelector('.source').textContent).toBe 'User'
|
||||
expect(row.querySelector('.selector').textContent).toBe '.editor'
|
||||
|
||||
describe "when searching key bindings", ->
|
||||
it "find case-insensitive results", ->
|
||||
keyBindings.push
|
||||
source: "#{atom.getLoadSettings().resourcePath}#{path.sep}keymaps", keystrokes: 'F11', command: 'window:toggle-full-screen', selector: 'body'
|
||||
atom.keymaps.emitter.emit 'did-reload-keymap'
|
||||
|
||||
panel.filterKeyBindings keyBindings, 'f11'
|
||||
|
||||
expect(panel.refs.keybindingRows.children.length).toBe 1
|
||||
|
||||
row = panel.refs.keybindingRows.children[0]
|
||||
expect(row.querySelector('.keystroke').textContent).toBe 'F11'
|
||||
expect(row.querySelector('.command').textContent).toBe 'window:toggle-full-screen'
|
||||
expect(row.querySelector('.source').textContent).toBe 'Core'
|
||||
expect(row.querySelector('.selector').textContent).toBe 'body'
|
||||
|
||||
it "perform a fuzzy match for each keyword", ->
|
||||
panel.filterKeyBindings keyBindings, 'core ctrl-a'
|
||||
|
||||
expect(panel.refs.keybindingRows.children.length).toBe 1
|
||||
|
||||
row = panel.refs.keybindingRows.children[0]
|
||||
expect(row.querySelector('.keystroke').textContent).toBe 'ctrl-a'
|
||||
expect(row.querySelector('.command').textContent).toBe 'core:select-all'
|
||||
expect(row.querySelector('.source').textContent).toBe 'Core'
|
||||
expect(row.querySelector('.selector').textContent).toBe '.editor, .platform-test'
|
35
packages/settings-view/spec/list-spec.coffee
Normal file
35
packages/settings-view/spec/list-spec.coffee
Normal file
@ -0,0 +1,35 @@
|
||||
List = require '../lib/list'
|
||||
|
||||
describe 'List', ->
|
||||
list = null
|
||||
|
||||
beforeEach ->
|
||||
list = new List('name')
|
||||
|
||||
it 'emits add and remove events when setting items', ->
|
||||
addHandler = jasmine.createSpy()
|
||||
removeHandler = jasmine.createSpy()
|
||||
list.onDidAddItem(addHandler)
|
||||
list.onDidRemoveItem(removeHandler)
|
||||
|
||||
items = [{name: 'one', text: 'a'}, {name: 'two', text: 'b'}]
|
||||
list.setItems(items)
|
||||
expect(addHandler.callCount).toBe 2
|
||||
expect(removeHandler.callCount).toBe 0
|
||||
|
||||
addHandler.reset()
|
||||
removeHandler.reset()
|
||||
|
||||
items = [{name: 'three', text: 'c'}, {name: 'two', text: 'b'}]
|
||||
list.setItems(items)
|
||||
expect(addHandler.callCount).toBe 1
|
||||
expect(removeHandler.callCount).toBe 1
|
||||
expect(addHandler.mostRecentCall.args[0]).toEqual {name: 'three', text: 'c'}
|
||||
expect(removeHandler.mostRecentCall.args[0]).toEqual {name: 'one', text: 'a'}
|
||||
expect(list.getItems()).toEqual items
|
||||
|
||||
addHandler.reset()
|
||||
removeHandler.reset()
|
||||
items.push {name: 'four'}
|
||||
list.setItems(items)
|
||||
expect(addHandler.callCount).toBe 1
|
67
packages/settings-view/spec/list-view-spec.coffee
Normal file
67
packages/settings-view/spec/list-view-spec.coffee
Normal file
@ -0,0 +1,67 @@
|
||||
List = require '../lib/list'
|
||||
ListView = require '../lib/list-view'
|
||||
|
||||
describe 'ListView', ->
|
||||
[list, view, container] = []
|
||||
|
||||
beforeEach ->
|
||||
list = new List('name')
|
||||
container = document.createElement('div')
|
||||
view = new ListView list, container, (item) ->
|
||||
element = document.createElement('div')
|
||||
element.classList.add(item.name)
|
||||
element.textContent = "#{item.name}|#{item.text}"
|
||||
{element, destroy: -> element.remove()}
|
||||
|
||||
it 'updates the list when the items are changed', ->
|
||||
expect(container.children.length).toBe 0
|
||||
|
||||
items = [{name: 'one', text: 'a'}, {name: 'two', text: 'b'}]
|
||||
list.setItems(items)
|
||||
expect(container.children.length).toBe 2
|
||||
expect(container.querySelector('.one').textContent).toBe 'one|a'
|
||||
expect(container.querySelector('.two').textContent).toBe 'two|b'
|
||||
|
||||
items = [{name: 'three', text: 'c'}, {name: 'two', text: 'b'}]
|
||||
list.setItems(items)
|
||||
expect(container.children.length).toBe 2
|
||||
expect(container.querySelector('.one')).not.toExist()
|
||||
expect(container.querySelector('.two').textContent).toBe 'two|b'
|
||||
expect(container.querySelector('.three').textContent).toBe 'three|c'
|
||||
|
||||
it 'filters views', ->
|
||||
items = [
|
||||
{name: 'one', text: '', filterText: 'x'},
|
||||
{name: 'two', text: '', filterText: 'y'}
|
||||
{name: 'three', text: '', filterText: 'x'}
|
||||
{name: 'four', text: '', filterText: 'z'}
|
||||
]
|
||||
|
||||
list.setItems(items)
|
||||
views = view.filterViews (item) -> item.filterText is 'x'
|
||||
|
||||
expect(views).toHaveLength 2
|
||||
expect(views[0].element.textContent).toBe 'one|'
|
||||
expect(views[1].element.textContent).toBe 'three|'
|
||||
|
||||
it 'filters views after an update', ->
|
||||
items = [
|
||||
{name: 'one', text: '', filterText: 'x'},
|
||||
{name: 'two', text: '', filterText: 'y'}
|
||||
{name: 'three', text: '', filterText: 'x'}
|
||||
{name: 'four', text: '', filterText: 'z'}
|
||||
]
|
||||
list.setItems(items)
|
||||
|
||||
items = [
|
||||
{name: 'one', text: '', filterText: 'x'},
|
||||
{name: 'two', text: '', filterText: 'y'}
|
||||
{name: 'three', text: '', filterText: 'x'}
|
||||
{name: 'four', text: '', filterText: 'z'}
|
||||
]
|
||||
list.setItems(items)
|
||||
views = view.filterViews (item) -> item.filterText is 'x'
|
||||
|
||||
expect(views).toHaveLength 2
|
||||
expect(views[0].element.textContent).toBe 'one|'
|
||||
expect(views[1].element.textContent).toBe 'three|'
|
563
packages/settings-view/spec/package-card-spec.coffee
Normal file
563
packages/settings-view/spec/package-card-spec.coffee
Normal file
@ -0,0 +1,563 @@
|
||||
path = require 'path'
|
||||
PackageCard = require '../lib/package-card'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
|
||||
describe "PackageCard", ->
|
||||
setPackageStatusSpies = (opts) ->
|
||||
spyOn(PackageCard.prototype, 'isInstalled').andReturn(opts.installed)
|
||||
spyOn(PackageCard.prototype, 'isDisabled').andReturn(opts.disabled)
|
||||
spyOn(PackageCard.prototype, 'hasSettings').andReturn(opts.hasSettings)
|
||||
|
||||
[card, packageManager] = []
|
||||
|
||||
beforeEach ->
|
||||
packageManager = new PackageManager()
|
||||
spyOn(packageManager, 'runCommand')
|
||||
|
||||
it "doesn't show the disable control for a theme", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false}
|
||||
card = new PackageCard({theme: 'syntax', name: 'test-theme'}, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.enablementButton).not.toBeVisible()
|
||||
|
||||
it "doesn't show the status indicator for a theme", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false}
|
||||
card = new PackageCard {theme: 'syntax', name: 'test-theme'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.statusIndicatorButton).not.toBeVisible()
|
||||
|
||||
it "doesn't show the settings button for a theme", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false}
|
||||
card = new PackageCard {theme: 'syntax', name: 'test-theme'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.settingsButton).not.toBeVisible()
|
||||
|
||||
it "doesn't show the settings button on the settings view", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false, hasSettings: true}
|
||||
card = new PackageCard {name: 'test-package'}, new SettingsView(), packageManager, {onSettingsView: true}
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.settingsButton).not.toBeVisible()
|
||||
|
||||
it "removes the settings button if a package has no settings", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false, hasSettings: false}
|
||||
card = new PackageCard {name: 'test-package'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.settingsButton).not.toBeVisible()
|
||||
|
||||
it "removes the uninstall button if a package has is a bundled package", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false, hasSettings: true}
|
||||
card = new PackageCard {name: 'find-and-replace'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.uninstallButton).not.toBeVisible()
|
||||
|
||||
it "displays the new version in the update button", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false, hasSettings: true}
|
||||
card = new PackageCard {name: 'find-and-replace', version: '1.0.0', latestVersion: '1.2.0'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.updateButton).toBeVisible()
|
||||
expect(card.refs.updateButton.textContent).toContain 'Update to 1.2.0'
|
||||
|
||||
it "displays the new version in the update button when the package is disabled", ->
|
||||
setPackageStatusSpies {installed: true, disabled: true, hasSettings: true}
|
||||
card = new PackageCard {name: 'find-and-replace', version: '1.0.0', latestVersion: '1.2.0'}, new SettingsView(), packageManager
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.updateButton).toBeVisible()
|
||||
expect(card.refs.updateButton.textContent).toContain 'Update to 1.2.0'
|
||||
|
||||
it "shows the author details", ->
|
||||
authorName = "authorName"
|
||||
pack =
|
||||
name: 'some-package'
|
||||
version: '0.1.0'
|
||||
repository: "https://github.com/#{authorName}/some-package"
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.loginLink.textContent).toBe(authorName)
|
||||
|
||||
describe "when the package is not installed", ->
|
||||
it "shows the settings, uninstall, and disable buttons", ->
|
||||
pack =
|
||||
name: 'some-package'
|
||||
version: '0.1.0'
|
||||
repository: 'http://github.com/omgwow/some-package'
|
||||
spyOn(PackageCard::, 'isDeprecated').andReturn(false)
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.installButtonGroup).toBeVisible()
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.packageActionButtonGroup).not.toBeVisible()
|
||||
|
||||
it "can be installed if currently not installed", ->
|
||||
setPackageStatusSpies {installed: false, disabled: false}
|
||||
spyOn(packageManager, 'install')
|
||||
|
||||
card = new PackageCard {name: 'test-package'}, new SettingsView(), packageManager
|
||||
expect(card.refs.installButton.style.display).not.toBe('none')
|
||||
expect(card.refs.uninstallButton.style.display).toBe('none')
|
||||
card.refs.installButton.click()
|
||||
expect(packageManager.install).toHaveBeenCalled()
|
||||
|
||||
it "can be installed if currently not installed and package latest release engine match atom version", ->
|
||||
spyOn(packageManager, 'install')
|
||||
spyOn(packageManager, 'loadCompatiblePackageVersion').andCallFake (packageName, callback) ->
|
||||
pack =
|
||||
name: packageName
|
||||
version: '0.1.0'
|
||||
engines:
|
||||
atom: '>0.50.0'
|
||||
|
||||
callback(null, pack)
|
||||
|
||||
setPackageStatusSpies {installed: false, disabled: false}
|
||||
|
||||
card = new PackageCard {
|
||||
name: 'test-package'
|
||||
version: '0.1.0'
|
||||
engines:
|
||||
atom: '>0.50.0'
|
||||
}, new SettingsView(), packageManager
|
||||
|
||||
# In that case there's no need to make a request to get all the versions
|
||||
expect(packageManager.loadCompatiblePackageVersion).not.toHaveBeenCalled()
|
||||
|
||||
expect(card.refs.installButton.style.display).not.toBe('none')
|
||||
expect(card.refs.uninstallButton.style.display).toBe('none')
|
||||
card.refs.installButton.click()
|
||||
expect(packageManager.install).toHaveBeenCalled()
|
||||
expect(packageManager.install.mostRecentCall.args[0]).toEqual({
|
||||
name: 'test-package'
|
||||
version: '0.1.0'
|
||||
engines:
|
||||
atom: '>0.50.0'
|
||||
})
|
||||
|
||||
it "can be installed with a previous version whose engine match the current atom version", ->
|
||||
spyOn(packageManager, 'install')
|
||||
spyOn(packageManager, 'loadCompatiblePackageVersion').andCallFake (packageName, callback) ->
|
||||
pack =
|
||||
name: packageName
|
||||
version: '0.0.1'
|
||||
engines:
|
||||
atom: '>0.50.0'
|
||||
|
||||
callback(null, pack)
|
||||
|
||||
setPackageStatusSpies {installed: false, disabled: false}
|
||||
|
||||
card = new PackageCard {
|
||||
name: 'test-package'
|
||||
version: '0.1.0'
|
||||
engines:
|
||||
atom: '>99.0.0'
|
||||
}, new SettingsView(), packageManager
|
||||
|
||||
expect(card.refs.installButton.style.display).not.toBe('none')
|
||||
expect(card.refs.uninstallButton.style.display).toBe('none')
|
||||
expect(card.refs.versionValue.textContent).toBe('0.0.1')
|
||||
expect(card.refs.versionValue).toHaveClass('text-warning')
|
||||
expect(card.refs.packageMessage).toHaveClass('text-warning')
|
||||
card.refs.installButton.click()
|
||||
expect(packageManager.install).toHaveBeenCalled()
|
||||
expect(packageManager.install.mostRecentCall.args[0]).toEqual({
|
||||
name: 'test-package'
|
||||
version: '0.0.1'
|
||||
engines:
|
||||
atom: '>0.50.0'
|
||||
})
|
||||
|
||||
it "can't be installed if there is no version compatible with the current atom version", ->
|
||||
spyOn(packageManager, 'loadCompatiblePackageVersion').andCallFake (packageName, callback) ->
|
||||
pack =
|
||||
name: packageName
|
||||
|
||||
callback(null, pack)
|
||||
|
||||
setPackageStatusSpies {installed: false, disabled: false}
|
||||
|
||||
pack =
|
||||
name: 'test-package'
|
||||
engines:
|
||||
atom: '>=99.0.0'
|
||||
card = new PackageCard(pack , new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.packageActionButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.versionValue).toHaveClass('text-error')
|
||||
expect(card.refs.packageMessage).toHaveClass('text-error')
|
||||
|
||||
describe "when the package is installed", ->
|
||||
beforeEach ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
waitsFor ->
|
||||
atom.packages.isPackageLoaded('package-with-config') is true
|
||||
|
||||
it "can be disabled if installed", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false}
|
||||
spyOn(atom.packages, 'disablePackage').andReturn(true)
|
||||
|
||||
card = new PackageCard {name: 'test-package'}, new SettingsView(), packageManager
|
||||
expect(card.refs.enablementButton.querySelector('.disable-text').textContent).toBe('Disable')
|
||||
card.refs.enablementButton.click()
|
||||
expect(atom.packages.disablePackage).toHaveBeenCalled()
|
||||
|
||||
it "can be updated", ->
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
pack.latestVersion = '1.1.0'
|
||||
packageUpdated = false
|
||||
|
||||
packageManager.on 'package-updated', -> packageUpdated = true
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
callback(0, '', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
originalLoadPackage = atom.packages.loadPackage
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake ->
|
||||
originalLoadPackage.call(atom.packages, path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.updateButton).toBeVisible()
|
||||
|
||||
card.update()
|
||||
|
||||
waitsFor ->
|
||||
packageUpdated
|
||||
|
||||
runs ->
|
||||
expect(card.refs.updateButton).not.toBeVisible()
|
||||
|
||||
it 'keeps the update button visible if the update failed', ->
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
pack.latestVersion = '1.1.0'
|
||||
updateFailed = false
|
||||
|
||||
packageManager.on 'package-update-failed', -> updateFailed = true
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
callback(1, '', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
originalLoadPackage = atom.packages.loadPackage
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake ->
|
||||
originalLoadPackage.call(atom.packages, path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.updateButton).toBeVisible()
|
||||
|
||||
card.update()
|
||||
|
||||
waitsFor ->
|
||||
updateFailed
|
||||
|
||||
runs ->
|
||||
expect(card.refs.updateButton).toBeVisible()
|
||||
|
||||
it 'does not error when attempting to update without any update available', ->
|
||||
# While this cannot be done through the package card UI,
|
||||
# updates can still be triggered through the Updates panel's Update All button
|
||||
# https://github.com/atom/settings-view/issues/879
|
||||
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
|
||||
originalLoadPackage = atom.packages.loadPackage
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake ->
|
||||
originalLoadPackage.call(atom.packages, path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
expect(card.refs.updateButton).not.toBeVisible()
|
||||
|
||||
waitsForPromise -> card.update()
|
||||
|
||||
runs ->
|
||||
expect(card.refs.updateButton).not.toBeVisible()
|
||||
|
||||
it "will stay disabled after an update", ->
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
pack.latestVersion = '1.1.0'
|
||||
packageUpdated = false
|
||||
|
||||
packageManager.on 'package-updated', -> packageUpdated = true
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
callback(0, '', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
originalLoadPackage = atom.packages.loadPackage
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake ->
|
||||
originalLoadPackage.call(atom.packages, path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
pack.disable()
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
expect(atom.packages.isPackageDisabled('package-with-config')).toBe true
|
||||
card.update()
|
||||
|
||||
waitsFor ->
|
||||
packageUpdated
|
||||
|
||||
runs ->
|
||||
expect(atom.packages.isPackageDisabled('package-with-config')).toBe true
|
||||
|
||||
it "is uninstalled when the uninstallButton is clicked", ->
|
||||
setPackageStatusSpies {installed: true, disabled: false}
|
||||
|
||||
[uninstallCallback] = []
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
if args[0] is 'uninstall'
|
||||
uninstallCallback = callback
|
||||
onWillThrowError: ->
|
||||
|
||||
spyOn(packageManager, 'install').andCallThrough()
|
||||
spyOn(packageManager, 'uninstall').andCallThrough()
|
||||
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
card.refs.uninstallButton.click()
|
||||
|
||||
expect(card.refs.uninstallButton.disabled).toBe true
|
||||
expect(card.refs.enablementButton.disabled).toBe true
|
||||
expect(card.refs.uninstallButton).toHaveClass('is-uninstalling')
|
||||
|
||||
expect(packageManager.uninstall).toHaveBeenCalled()
|
||||
expect(packageManager.uninstall.mostRecentCall.args[0].name).toEqual('package-with-config')
|
||||
|
||||
jasmine.unspy(PackageCard::, 'isInstalled')
|
||||
spyOn(PackageCard.prototype, 'isInstalled').andReturn false
|
||||
uninstallCallback(0, '', '')
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(card.refs.uninstallButton.disabled).toBe false
|
||||
expect(card.refs.uninstallButton).not.toHaveClass('is-uninstalling')
|
||||
expect(card.refs.installButtonGroup).toBeVisible()
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.packageActionButtonGroup).not.toBeVisible()
|
||||
|
||||
it "shows the settings, uninstall, and enable buttons when disabled", ->
|
||||
atom.config.set('package-with-config.setting', 'something')
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
spyOn(atom.packages, 'isPackageDisabled').andReturn(true)
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
|
||||
expect(card.refs.settingsButton).toBeVisible()
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Enable'
|
||||
|
||||
it "shows the settings, uninstall, and disable buttons", ->
|
||||
atom.config.set('package-with-config.setting', 'something')
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
spyOn(PackageCard::, 'isDeprecated').andReturn(false)
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
|
||||
expect(card.refs.settingsButton).toBeVisible()
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Disable'
|
||||
|
||||
it "does not show the settings button when there are no settings", ->
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
spyOn(PackageCard::, 'isDeprecated').andReturn(false)
|
||||
spyOn(PackageCard::, 'hasSettings').andReturn(false)
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
expect(card.refs.settingsButton).not.toBeVisible()
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Disable'
|
||||
|
||||
###
|
||||
hasDeprecations, no update: disabled-settings, uninstall, disable
|
||||
hasDeprecations, has update: update, disabled-settings, uninstall, disable
|
||||
###
|
||||
describe "when the package has deprecations", ->
|
||||
beforeEach ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
|
||||
waitsFor ->
|
||||
atom.packages.isPackageLoaded('package-with-config') is true
|
||||
|
||||
runs ->
|
||||
atom.config.set('package-with-config.setting', 'something')
|
||||
|
||||
describe "when hasDeprecations is true and NO update is available", ->
|
||||
beforeEach ->
|
||||
spyOn(PackageCard::, 'isDeprecated').andReturn true
|
||||
spyOn(PackageCard::, 'isInstalled').andReturn true
|
||||
spyOn(PackageCard::, 'getDeprecatedPackageMetadata').andReturn
|
||||
hasDeprecations: true
|
||||
version: '<=1.0.0'
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
pack.version = pack.metadata.version
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
it "shows the correct state", ->
|
||||
spyOn(atom.packages, 'isPackageDisabled').andReturn false
|
||||
card.updateInterfaceState()
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
|
||||
expect(card.element).toHaveClass 'deprecated'
|
||||
expect(card.refs.packageMessage.textContent).toContain 'no update available'
|
||||
expect(card.refs.packageMessage).toHaveClass 'text-warning'
|
||||
expect(card.refs.settingsButton.disabled).toBe true
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Disable'
|
||||
expect(card.refs.enablementButton.disabled).toBe false
|
||||
|
||||
it "displays a disabled enable button when the package is disabled", ->
|
||||
spyOn(atom.packages, 'isPackageDisabled').andReturn true
|
||||
card.updateInterfaceState()
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
|
||||
expect(card.element).toHaveClass 'deprecated'
|
||||
expect(card.refs.packageMessage.textContent).toContain 'no update available'
|
||||
expect(card.refs.packageMessage).toHaveClass 'text-warning'
|
||||
expect(card.refs.settingsButton.disabled).toBe true
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Enable'
|
||||
expect(card.refs.enablementButton.disabled).toBe true
|
||||
|
||||
# NOTE: the mocking here is pretty delicate
|
||||
describe "when hasDeprecations is true and there is an update is available", ->
|
||||
beforeEach ->
|
||||
spyOn(PackageCard::, 'isDeprecated').andCallFake (version) ->
|
||||
semver = require 'semver'
|
||||
version = version ? card?.pack?.version ? '1.0.0'
|
||||
semver.satisfies(version, '<=1.0.1')
|
||||
spyOn(PackageCard::, 'getDeprecatedPackageMetadata').andReturn
|
||||
hasDeprecations: true
|
||||
version: '<=1.0.1'
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
pack.version = pack.metadata.version
|
||||
card = new PackageCard(pack, new SettingsView(), packageManager)
|
||||
jasmine.attachToDOM(card.element)
|
||||
|
||||
it "explains that the update WILL NOT fix the deprecations when the new version isnt higher than the max version", ->
|
||||
card.displayAvailableUpdate('1.0.1')
|
||||
expect(card.refs.packageMessage.textContent).not.toContain 'no update available'
|
||||
expect(card.refs.packageMessage.textContent).toContain 'still contains deprecations'
|
||||
|
||||
describe "when the available update fixes deprecations", ->
|
||||
it "explains that the update WILL fix the deprecations when the new version is higher than the max version", ->
|
||||
card.displayAvailableUpdate('1.1.0')
|
||||
expect(card.refs.packageMessage.textContent).not.toContain 'no update available'
|
||||
expect(card.refs.packageMessage.textContent).toContain 'without deprecations'
|
||||
|
||||
expect(card.refs.updateButtonGroup).toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.packageActionButtonGroup).toBeVisible()
|
||||
expect(card.refs.uninstallButton).toBeVisible()
|
||||
expect(card.refs.enablementButton).toBeVisible()
|
||||
expect(card.refs.enablementButton.textContent).toBe 'Disable'
|
||||
|
||||
it "updates the package and shows a restart notification when the update button is clicked", ->
|
||||
expect(atom.packages.getLoadedPackage('package-with-config')).toBeTruthy()
|
||||
|
||||
[updateCallback] = []
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
updateCallback = callback
|
||||
onWillThrowError: ->
|
||||
spyOn(packageManager, 'update').andCallThrough()
|
||||
|
||||
originalLoadPackage = atom.packages.loadPackage
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake ->
|
||||
pack = originalLoadPackage.call(atom.packages, path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
pack.metadata.version = '1.1.0' if pack?
|
||||
pack
|
||||
|
||||
card.pack.latestVersion = "1.1.0"
|
||||
card.displayAvailableUpdate('1.1.0')
|
||||
expect(card.refs.updateButtonGroup).toBeVisible()
|
||||
|
||||
expect(atom.packages.getLoadedPackage('package-with-config')).toBeTruthy()
|
||||
card.refs.updateButton.click()
|
||||
|
||||
expect(card.refs.updateButton.disabled).toBe true
|
||||
expect(card.refs.updateButton).toHaveClass 'is-installing'
|
||||
|
||||
expect(packageManager.update).toHaveBeenCalled()
|
||||
expect(packageManager.update.mostRecentCall.args[0].name).toEqual 'package-with-config'
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(card.element).toHaveClass 'deprecated'
|
||||
|
||||
expect(card.refs.updateButtonGroup).toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
|
||||
updateCallback(0, '', '')
|
||||
|
||||
waits 0 # Wait for PackageCard.update promise to resolve
|
||||
|
||||
runs ->
|
||||
expect(card.refs.updateButton.disabled).toBe false
|
||||
expect(card.refs.updateButton).not.toHaveClass 'is-installing'
|
||||
expect(card.refs.updateButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.installButtonGroup).not.toBeVisible()
|
||||
expect(card.refs.packageActionButtonGroup).toBeVisible()
|
||||
expect(card.refs.versionValue.textContent).toBe '1.0.0' # Does not update until restart
|
||||
|
||||
notifications = atom.notifications.getNotifications()
|
||||
expect(notifications.length).toBe 1
|
||||
notif = notifications[0]
|
||||
|
||||
expect(notif.options.detail).toBe "1.0.0 -> 1.1.0"
|
||||
expect(notif.options.buttons.length).toBe(2)
|
||||
|
||||
spyOn(atom, 'restartApplication')
|
||||
notif.options.buttons[0].onDidClick()
|
||||
expect(atom.restartApplication).toHaveBeenCalled()
|
||||
|
||||
spyOn(notif, 'dismiss')
|
||||
notif.options.buttons[1].onDidClick()
|
||||
expect(notif.dismiss).toHaveBeenCalled()
|
||||
|
||||
it "shows the sha in the notification when a git url package is updated", ->
|
||||
expect(atom.packages.getLoadedPackage('package-with-config')).toBeTruthy()
|
||||
|
||||
[updateCallback] = []
|
||||
packageManager.runCommand.andCallFake (args, callback) ->
|
||||
updateCallback = callback
|
||||
onWillThrowError: ->
|
||||
spyOn(packageManager, 'update').andCallThrough()
|
||||
|
||||
card.pack.apmInstallSource = {type: 'git', sha: 'cf23df2207d99a74fbe169e3eba035e633b65d94'}
|
||||
card.pack.latestSha = 'a296114f3d0deec519a41f4c62e7fc56075b7f01'
|
||||
|
||||
card.displayAvailableUpdate('1.1.0')
|
||||
expect(card.refs.updateButtonGroup).toBeVisible()
|
||||
|
||||
expect(atom.packages.getLoadedPackage('package-with-config')).toBeTruthy()
|
||||
card.refs.updateButton.click()
|
||||
|
||||
updateCallback(0, '', '')
|
||||
|
||||
waits 0 # Wait for PackageCard.update promise to resolve
|
||||
|
||||
runs ->
|
||||
notifications = atom.notifications.getNotifications()
|
||||
expect(notifications.length).toBe 1
|
||||
expect(notifications[0].options.detail).toBe "cf23df22 -> a296114f"
|
156
packages/settings-view/spec/package-detail-view-spec.coffee
Normal file
156
packages/settings-view/spec/package-detail-view-spec.coffee
Normal file
@ -0,0 +1,156 @@
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
{shell} = require 'electron'
|
||||
|
||||
PackageDetailView = require '../lib/package-detail-view'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
AtomIoClient = require '../lib/atom-io-client'
|
||||
SnippetsProvider =
|
||||
getSnippets: -> {}
|
||||
|
||||
describe "PackageDetailView", ->
|
||||
packageManager = null
|
||||
view = null
|
||||
|
||||
createClientSpy = ->
|
||||
jasmine.createSpyObj('client', ['package', 'avatar'])
|
||||
|
||||
beforeEach ->
|
||||
packageManager = new PackageManager
|
||||
view = null
|
||||
|
||||
loadPackageFromRemote = (packageName, opts) ->
|
||||
opts ?= {}
|
||||
packageManager.client = createClientSpy()
|
||||
packageManager.client.package.andCallFake (name, cb) ->
|
||||
packageData = require(path.join(__dirname, 'fixtures', packageName, 'package.json'))
|
||||
packageData.readme = fs.readFileSync(path.join(__dirname, 'fixtures', packageName, 'README.md'), 'utf8')
|
||||
cb(null, packageData)
|
||||
view = new PackageDetailView({name: packageName}, new SettingsView(), packageManager, SnippetsProvider)
|
||||
view.beforeShow(opts)
|
||||
|
||||
loadCustomPackageFromRemote = (packageName, opts) ->
|
||||
opts ?= {}
|
||||
packageManager.client = createClientSpy()
|
||||
packageManager.client.package.andCallFake (name, cb) ->
|
||||
packageData = require(path.join(__dirname, 'fixtures', packageName, 'package.json'))
|
||||
cb(null, packageData)
|
||||
view = new PackageDetailView({name: packageName}, new SettingsView(), packageManager, SnippetsProvider)
|
||||
view.beforeShow(opts)
|
||||
|
||||
it "renders a package when provided in `initialize`", ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
pack = atom.packages.getLoadedPackage('package-with-config')
|
||||
view = new PackageDetailView(pack, new SettingsView(), packageManager, SnippetsProvider)
|
||||
|
||||
# Perhaps there are more things to assert here.
|
||||
expect(view.refs.title.textContent).toBe('Package With Config')
|
||||
|
||||
it "does not call the atom.io api for package metadata when present", ->
|
||||
packageManager.client = createClientSpy()
|
||||
view = new PackageDetailView({name: 'package-with-config'}, new SettingsView(), packageManager, SnippetsProvider)
|
||||
|
||||
# PackageCard is a subview, and it calls AtomIoClient::package once to load
|
||||
# metadata from the cache.
|
||||
expect(packageManager.client.package.callCount).toBe(1)
|
||||
|
||||
it "shows a loading message and calls out to atom.io when package metadata is missing", ->
|
||||
loadPackageFromRemote('package-with-readme')
|
||||
expect(view.refs.loadingMessage).not.toBe(null)
|
||||
expect(view.refs.loadingMessage.classList.contains('hidden')).not.toBe(true)
|
||||
expect(packageManager.client.package).toHaveBeenCalled()
|
||||
|
||||
it "shows an error when package metadata cannot be loaded via the API", ->
|
||||
packageManager.client = createClientSpy()
|
||||
packageManager.client.package.andCallFake (name, cb) ->
|
||||
error = new Error('API error')
|
||||
cb(error, null)
|
||||
|
||||
view = new PackageDetailView({name: 'nonexistent-package'}, new SettingsView(), packageManager, SnippetsProvider)
|
||||
|
||||
expect(view.refs.errorMessage.classList.contains('hidden')).not.toBe(true)
|
||||
expect(view.refs.loadingMessage.classList.contains('hidden')).toBe(true)
|
||||
expect(view.element.querySelectorAll('.package-card').length).toBe(0)
|
||||
|
||||
it "shows an error when package metadata cannot be loaded from the cache and the network is unavailable", ->
|
||||
localStorage.removeItem('settings-view:packages/some-package')
|
||||
|
||||
spyOn(AtomIoClient.prototype, 'online').andReturn(false)
|
||||
spyOn(AtomIoClient.prototype, 'request').andCallFake (path, callback) ->
|
||||
callback(new Error('getaddrinfo ENOENT atom.io:443'))
|
||||
spyOn(AtomIoClient.prototype, 'fetchFromCache').andCallThrough()
|
||||
|
||||
view = new PackageDetailView({name: 'some-package'}, new SettingsView(), packageManager, SnippetsProvider)
|
||||
|
||||
expect(AtomIoClient.prototype.fetchFromCache).toHaveBeenCalled()
|
||||
|
||||
expect(view.refs.errorMessage.classList.contains('hidden')).not.toBe(true)
|
||||
expect(view.refs.loadingMessage.classList.contains('hidden')).toBe(true)
|
||||
expect(view.element.querySelectorAll('.package-card').length).toBe(0)
|
||||
|
||||
it "renders the README successfully after a call to the atom.io api", ->
|
||||
loadPackageFromRemote('package-with-readme')
|
||||
expect(view.packageCard).toBeDefined()
|
||||
expect(view.packageCard.refs.packageName.textContent).toBe('package-with-readme')
|
||||
expect(view.element.querySelectorAll('.package-readme').length).toBe(1)
|
||||
|
||||
it "renders the README successfully with sanitized html", ->
|
||||
loadPackageFromRemote('package-with-readme')
|
||||
expect(view.element.querySelectorAll('.package-readme script').length).toBe(0)
|
||||
expect(view.element.querySelectorAll('.package-readme iframe').length).toBe(0)
|
||||
expect(view.element.querySelectorAll('.package-readme input[type="checkbox"][disabled]').length).toBe(2)
|
||||
expect(view.element.querySelector('img[alt="AbsoluteImage"]').getAttribute('src')).toBe('https://example.com/static/image.jpg')
|
||||
expect(view.element.querySelector('img[alt="RelativeImage"]').getAttribute('src')).toBe('https://github.com/example/package-with-readme/blob/master/static/image.jpg')
|
||||
expect(view.element.querySelector('img[alt="Base64Image"]').getAttribute('src')).toBe('data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')
|
||||
|
||||
it "renders the README when the package path is undefined", ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
|
||||
pack = atom.packages.getLoadedPackage('package-with-readme')
|
||||
delete pack.path
|
||||
view = new PackageDetailView(pack, new SettingsView(), packageManager, SnippetsProvider)
|
||||
|
||||
expect(view.packageCard).toBeDefined()
|
||||
expect(view.packageCard.refs.packageName.textContent).toBe('package-with-readme')
|
||||
expect(view.element.querySelectorAll('.package-readme').length).toBe(1)
|
||||
|
||||
it "triggers a report issue button click and checks that the fallback repository issue tracker URL was opened", ->
|
||||
loadCustomPackageFromRemote('package-without-bugs-property')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.issueButton.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/example/package-without-bugs-property/issues/new')
|
||||
|
||||
it "triggers a report issue button click and checks that the bugs URL string was opened", ->
|
||||
loadCustomPackageFromRemote('package-with-bugs-property-url-string')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.issueButton.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new')
|
||||
|
||||
it "triggers a report issue button click and checks that the bugs URL was opened", ->
|
||||
loadCustomPackageFromRemote('package-with-bugs-property-url')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.issueButton.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new')
|
||||
|
||||
it "triggers a report issue button click and checks that the bugs email link was opened", ->
|
||||
loadCustomPackageFromRemote('package-with-bugs-property-email')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.issueButton.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('mailto:issues@example.com')
|
||||
|
||||
it "should show 'Install' as the first breadcrumb by default", ->
|
||||
loadPackageFromRemote('package-with-readme')
|
||||
expect(view.refs.breadcrumb.textContent).toBe('Install')
|
||||
|
||||
it "should open repository url", ->
|
||||
loadPackageFromRemote('package-with-readme')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.packageRepo.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/example/package-with-readme')
|
||||
|
||||
it "should open internal package repository url", ->
|
||||
loadPackageFromRemote('package-internal')
|
||||
spyOn(shell, 'openExternal')
|
||||
view.refs.packageRepo.click()
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/atom/atom/tree/master/packages/package-internal')
|
||||
|
305
packages/settings-view/spec/package-manager-spec.coffee
Normal file
305
packages/settings-view/spec/package-manager-spec.coffee
Normal file
@ -0,0 +1,305 @@
|
||||
path = require 'path'
|
||||
process = require 'process'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
|
||||
describe "PackageManager", ->
|
||||
[packageManager] = []
|
||||
|
||||
beforeEach ->
|
||||
spyOn(atom.packages, 'getApmPath').andReturn('/an/invalid/apm/command/to/run')
|
||||
atom.config.set('core.useProxySettingsWhenCallingApm', false)
|
||||
packageManager = new PackageManager()
|
||||
|
||||
it "handle errors spawning apm", ->
|
||||
noSuchCommandError = if process.platform is 'win32' then ' cannot find the path ' else 'ENOENT'
|
||||
waitsForPromise shouldReject: true, -> packageManager.getInstalled()
|
||||
waitsForPromise shouldReject: true, -> packageManager.getOutdated()
|
||||
waitsForPromise shouldReject: true, -> packageManager.getFeatured()
|
||||
waitsForPromise shouldReject: true, -> packageManager.getPackage('foo')
|
||||
|
||||
installCallback = jasmine.createSpy('installCallback')
|
||||
uninstallCallback = jasmine.createSpy('uninstallCallback')
|
||||
updateCallback = jasmine.createSpy('updateCallback')
|
||||
|
||||
runs ->
|
||||
packageManager.install {name: 'foo', version: '1.0.0'}, installCallback
|
||||
|
||||
waitsFor ->
|
||||
installCallback.callCount is 1
|
||||
|
||||
runs ->
|
||||
installArg = installCallback.argsForCall[0][0]
|
||||
expect(installArg.message).toBe "Installing \u201Cfoo@1.0.0\u201D failed."
|
||||
expect(installArg.packageInstallError).toBe true
|
||||
expect(installArg.stderr).toContain noSuchCommandError
|
||||
|
||||
packageManager.uninstall {name: 'foo'}, uninstallCallback
|
||||
|
||||
waitsFor ->
|
||||
uninstallCallback.callCount is 1
|
||||
|
||||
runs ->
|
||||
uninstallArg = uninstallCallback.argsForCall[0][0]
|
||||
expect(uninstallArg.message).toBe "Uninstalling \u201Cfoo\u201D failed."
|
||||
expect(uninstallArg.stderr).toContain noSuchCommandError
|
||||
|
||||
packageManager.update {name: 'foo'}, '1.0.0', updateCallback
|
||||
|
||||
waitsFor ->
|
||||
updateCallback.callCount is 1
|
||||
|
||||
runs ->
|
||||
updateArg = updateCallback.argsForCall[0][0]
|
||||
expect(updateArg.message).toBe "Updating to \u201Cfoo@1.0.0\u201D failed."
|
||||
expect(updateArg.packageInstallError).toBe true
|
||||
expect(updateArg.stderr).toContain noSuchCommandError
|
||||
|
||||
describe "::isPackageInstalled()", ->
|
||||
it "returns false a package is not installed", ->
|
||||
expect(packageManager.isPackageInstalled('some-package')).toBe false
|
||||
|
||||
it "returns true when a package is loaded", ->
|
||||
spyOn(atom.packages, 'isPackageLoaded').andReturn true
|
||||
expect(packageManager.isPackageInstalled('some-package')).toBe true
|
||||
|
||||
it "returns true when a package is disabled", ->
|
||||
spyOn(atom.packages, 'getAvailablePackageNames').andReturn ['some-package']
|
||||
expect(packageManager.isPackageInstalled('some-package')).toBe true
|
||||
|
||||
describe "::install()", ->
|
||||
[runArgs, runCallback] = []
|
||||
|
||||
beforeEach ->
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
runArgs = args
|
||||
runCallback = callback
|
||||
onWillThrowError: ->
|
||||
|
||||
it "installs the latest version when a package version is not specified", ->
|
||||
packageManager.install {name: 'something'}, ->
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(runArgs).toEqual ['install', 'something', '--json']
|
||||
|
||||
it "installs the package@version when a version is specified", ->
|
||||
packageManager.install {name: 'something', version: '0.2.3'}, ->
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(runArgs).toEqual ['install', 'something@0.2.3', '--json']
|
||||
|
||||
describe "git url installation", ->
|
||||
it 'installs https:// urls', ->
|
||||
url = "https://github.com/user/repo.git"
|
||||
packageManager.install {name: url}
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(runArgs).toEqual ['install', 'https://github.com/user/repo.git', '--json']
|
||||
|
||||
it 'installs git@ urls', ->
|
||||
url = "git@github.com:user/repo.git"
|
||||
packageManager.install {name: url}
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(runArgs).toEqual ['install', 'git@github.com:user/repo.git', '--json']
|
||||
|
||||
it 'installs user/repo url shortcuts', ->
|
||||
url = "user/repo"
|
||||
packageManager.install {name: url}
|
||||
expect(packageManager.runCommand).toHaveBeenCalled()
|
||||
expect(runArgs).toEqual ['install', 'user/repo', '--json']
|
||||
|
||||
it 'installs and activates git pacakges with names different from the repo name', ->
|
||||
spyOn(atom.packages, 'activatePackage')
|
||||
packageManager.install(name: 'git-repo-name')
|
||||
json =
|
||||
metadata:
|
||||
name: 'real-package-name'
|
||||
runCallback(0, JSON.stringify([json]), '')
|
||||
expect(atom.packages.activatePackage).toHaveBeenCalledWith json.metadata.name
|
||||
|
||||
it 'emits an installed event with a copy of the pack including the full package metadata', ->
|
||||
spyOn(packageManager, 'emitPackageEvent')
|
||||
originalPackObject = name: 'git-repo-name', otherData: {will: 'beCopied'}
|
||||
packageManager.install(originalPackObject)
|
||||
json =
|
||||
metadata:
|
||||
name: 'real-package-name'
|
||||
moreInfo: 'yep'
|
||||
runCallback(0, JSON.stringify([json]), '')
|
||||
|
||||
installEmittedCount = 0
|
||||
for call in packageManager.emitPackageEvent.calls
|
||||
if call.args[0] is "installed"
|
||||
expect(call.args[1]).not.toEqual originalPackObject
|
||||
expect(call.args[1].moreInfo).toEqual "yep"
|
||||
expect(call.args[1].otherData).toBe originalPackObject.otherData
|
||||
installEmittedCount++
|
||||
expect(installEmittedCount).toBe 1
|
||||
|
||||
describe "::uninstall()", ->
|
||||
[runCallback] = []
|
||||
|
||||
beforeEach ->
|
||||
spyOn(packageManager, 'unload')
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
runCallback = callback
|
||||
onWillThrowError: ->
|
||||
|
||||
it "removes the package from the core.disabledPackages list", ->
|
||||
atom.config.set('core.disabledPackages', ['something'])
|
||||
|
||||
packageManager.uninstall {name: 'something'}, ->
|
||||
|
||||
expect(atom.config.get('core.disabledPackages')).toContain('something')
|
||||
runCallback(0, '', '')
|
||||
expect(atom.config.get('core.disabledPackages')).not.toContain('something')
|
||||
|
||||
describe "::packageHasSettings", ->
|
||||
it "returns true when the pacakge has config", ->
|
||||
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
|
||||
expect(packageManager.packageHasSettings('package-with-config')).toBe true
|
||||
|
||||
it "returns false when the pacakge does not have config and doesn't define language grammars", ->
|
||||
expect(packageManager.packageHasSettings('random-package')).toBe false
|
||||
|
||||
it "returns true when the pacakge does not have config, but does define language grammars", ->
|
||||
packageName = 'language-test'
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', packageName))
|
||||
|
||||
runs ->
|
||||
expect(packageManager.packageHasSettings(packageName)).toBe true
|
||||
|
||||
describe "::loadOutdated", ->
|
||||
it "caches results", ->
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.apmCache.loadOutdated.value).toMatch([{"name": "boop"}])
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(1)
|
||||
|
||||
it "expires results after a timeout", ->
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
now = Date.now()
|
||||
spyOn(Date, 'now') unless Date.now.andReturn
|
||||
Date.now.andReturn((-> now + packageManager.CACHE_EXPIRY + 1)())
|
||||
packageManager.loadOutdated false, ->
|
||||
|
||||
expect(packageManager.runCommand.calls.length).toBe(2)
|
||||
|
||||
it "expires results after a package updated/installed", ->
|
||||
packageManager.apmCache.loadOutdated =
|
||||
value: ['hi']
|
||||
expiry: Date.now() + 999999999
|
||||
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
# Just prevent this stuff from calling through, it doesn't matter for this test
|
||||
spyOn(atom.packages, 'deactivatePackage').andReturn(true)
|
||||
spyOn(atom.packages, 'activatePackage').andReturn(true)
|
||||
spyOn(atom.packages, 'unloadPackage').andReturn(true)
|
||||
spyOn(atom.packages, 'loadPackage').andReturn(true)
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(0)
|
||||
|
||||
packageManager.update {}, {}, -> # +1 runCommand call to update the package
|
||||
packageManager.loadOutdated false, -> # +1 runCommand call to load outdated because the cache should be wiped
|
||||
expect(packageManager.runCommand.calls.length).toBe(2)
|
||||
|
||||
packageManager.install {}, -> # +1 runCommand call to install the package
|
||||
packageManager.loadOutdated false, -> # +1 runCommand call to load outdated because the cache should be wiped
|
||||
expect(packageManager.runCommand.calls.length).toBe(4)
|
||||
|
||||
packageManager.loadOutdated false, -> # +0 runCommand call, should be cached
|
||||
expect(packageManager.runCommand.calls.length).toBe(4)
|
||||
|
||||
it "expires results if it is called with clearCache set to true", ->
|
||||
packageManager.apmCache.loadOutdated =
|
||||
value: ['hi']
|
||||
expiry: Date.now() + 999999999
|
||||
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated true, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(1)
|
||||
expect(packageManager.apmCache.loadOutdated.value).toEqual [{"name": "boop"}]
|
||||
|
||||
describe "when there is a version pinned package", ->
|
||||
beforeEach ->
|
||||
atom.config.set('core.versionPinnedPackages', ['beep'])
|
||||
|
||||
it "caches results", ->
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}, {"name": "beep"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.apmCache.loadOutdated.value).toMatch([{"name": "boop"}])
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(1)
|
||||
|
||||
it "expires results after a timeout", ->
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}, {"name": "beep"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
now = Date.now()
|
||||
spyOn(Date, 'now') unless Date.now.andReturn
|
||||
Date.now.andReturn((-> now + packageManager.CACHE_EXPIRY + 1)())
|
||||
packageManager.loadOutdated false, ->
|
||||
|
||||
expect(packageManager.runCommand.calls.length).toBe(2)
|
||||
|
||||
it "expires results after a package updated/installed", ->
|
||||
packageManager.apmCache.loadOutdated =
|
||||
value: ['hi']
|
||||
expiry: Date.now() + 999999999
|
||||
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}, {"name": "beep"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
# Just prevent this stuff from calling through, it doesn't matter for this test
|
||||
spyOn(atom.packages, 'deactivatePackage').andReturn(true)
|
||||
spyOn(atom.packages, 'activatePackage').andReturn(true)
|
||||
spyOn(atom.packages, 'unloadPackage').andReturn(true)
|
||||
spyOn(atom.packages, 'loadPackage').andReturn(true)
|
||||
|
||||
packageManager.loadOutdated false, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(0)
|
||||
|
||||
packageManager.update {}, {}, -> # +1 runCommand call to update the package
|
||||
packageManager.loadOutdated false, -> # +1 runCommand call to load outdated because the cache should be wiped
|
||||
expect(packageManager.runCommand.calls.length).toBe(2)
|
||||
|
||||
packageManager.install {}, -> # +1 runCommand call to install the package
|
||||
packageManager.loadOutdated false, -> # +1 runCommand call to load outdated because the cache should be wiped
|
||||
expect(packageManager.runCommand.calls.length).toBe(4)
|
||||
|
||||
packageManager.loadOutdated false, -> # +0 runCommand call, should be cached
|
||||
expect(packageManager.runCommand.calls.length).toBe(4)
|
||||
|
||||
it "expires results if it is called with clearCache set to true", ->
|
||||
packageManager.apmCache.loadOutdated =
|
||||
value: ['hi']
|
||||
expiry: Date.now() + 999999999
|
||||
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
callback(0, '[{"name": "boop"}, {"name": "beep"}]', '')
|
||||
onWillThrowError: ->
|
||||
|
||||
packageManager.loadOutdated true, ->
|
||||
expect(packageManager.runCommand.calls.length).toBe(1)
|
||||
expect(packageManager.apmCache.loadOutdated.value).toEqual [{"name": "boop"}]
|
@ -0,0 +1,152 @@
|
||||
PackageManager = require '../lib/package-manager'
|
||||
PackageUpdatesStatusView = require '../lib/package-updates-status-view'
|
||||
|
||||
describe "PackageUpdatesStatusView", ->
|
||||
[statusBar, statusView, packageManager] = []
|
||||
|
||||
outdatedPackage1 =
|
||||
name: 'out-dated-1'
|
||||
outdatedPackage2 =
|
||||
name: 'out-dated-2'
|
||||
installedPackage =
|
||||
name: 'user-package'
|
||||
|
||||
beforeEach ->
|
||||
spyOn(PackageManager.prototype, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(PackageManager.prototype, 'getInstalled').andCallFake -> Promise.resolve([installedPackage])
|
||||
spyOn(PackageManager.prototype, 'getOutdated').andCallFake -> Promise.resolve([outdatedPackage1, outdatedPackage2])
|
||||
spyOn(PackageUpdatesStatusView.prototype, 'initialize').andCallThrough()
|
||||
jasmine.attachToDOM(atom.views.getView(atom.workspace))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('status-bar')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('settings-view')
|
||||
|
||||
runs ->
|
||||
atom.packages.emitter.emit('did-activate-all')
|
||||
expect(document.querySelector('status-bar .package-updates-status-view')).toExist()
|
||||
|
||||
packageManager = PackageUpdatesStatusView.prototype.initialize.mostRecentCall.args[1]
|
||||
|
||||
describe "when packages are outdated", ->
|
||||
it "adds a tile to the status bar", ->
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates'
|
||||
|
||||
describe "when the tile is clicked", ->
|
||||
it "opens the Available Updates panel", ->
|
||||
spyOn(atom.commands, 'dispatch').andCallFake ->
|
||||
|
||||
document.querySelector('status-bar .package-updates-status-view').click()
|
||||
expect(atom.commands.dispatch).toHaveBeenCalledWith(atom.views.getView(atom.workspace), 'settings-view:check-for-package-updates')
|
||||
|
||||
it "does not destroy the tile", ->
|
||||
document.querySelector('status-bar .package-updates-status-view').click()
|
||||
expect(document.querySelector('status-bar .package-updates-status-view')).toExist()
|
||||
|
||||
describe "when a package is updating", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1/2 updating'
|
||||
|
||||
describe "when a package finishes updating", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when a package is updated without a prior updating event", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when multiple packages are updating and one finishes", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage2)
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1/1 updating'
|
||||
|
||||
describe "when a package fails to update", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates (1 failed)'
|
||||
|
||||
describe "when a package that previously failed to update starts updating again", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1/2 updating'
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates (1 failed)'
|
||||
|
||||
describe "when a package update that previously failed succeeds on a subsequent try", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when multiple events are happening at the same time", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('update-available', installedPackage)
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage2)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1/3 updating (1 failed)'
|
||||
|
||||
describe "when there are no more updates", ->
|
||||
it "destroys the tile", ->
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage2)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view')).not.toExist()
|
||||
|
||||
describe "when a new update becomes available and the tile is destroyed", ->
|
||||
it "recreates the tile", ->
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('updated', outdatedPackage2)
|
||||
packageManager.emitPackageEvent('update-available', installedPackage)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view')).toExist()
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when an update becomes available for a package", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('update-available', installedPackage)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '3 updates'
|
||||
|
||||
describe "when updates are checked for multiple times and no new updates are available", ->
|
||||
it "does not keep updating the tile", ->
|
||||
packageManager.emitPackageEvent('update-available', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-available', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-available', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates'
|
||||
|
||||
# There are more fields in an actual package object,
|
||||
# so make sure only name is tested and not object equality
|
||||
packageManager.emitPackageEvent('update-available', {name: 'out-dated-1', date: Date.now()})
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates'
|
||||
|
||||
describe "when the same update fails multiple times", ->
|
||||
it "does not keep updating the tile", ->
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '2 updates (1 failed)'
|
||||
|
||||
describe "when a package that can be updated is uninstalled", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('uninstalled', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when a package that is updating is uninstalled", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('updating', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('uninstalled', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
||||
|
||||
describe "when a package that failed to update is uninstalled", ->
|
||||
it "updates the tile", ->
|
||||
packageManager.emitPackageEvent('update-failed', outdatedPackage1)
|
||||
packageManager.emitPackageEvent('uninstalled', outdatedPackage1)
|
||||
expect(document.querySelector('status-bar .package-updates-status-view').textContent).toBe '1 update'
|
159
packages/settings-view/spec/rich-description-spec.coffee
Normal file
159
packages/settings-view/spec/rich-description-spec.coffee
Normal file
@ -0,0 +1,159 @@
|
||||
{getSettingDescription} = require '../lib/rich-description'
|
||||
|
||||
describe "Rich descriptions", ->
|
||||
beforeEach ->
|
||||
config =
|
||||
type: 'object'
|
||||
properties:
|
||||
plainText:
|
||||
description: 'Plain text description'
|
||||
type: 'string'
|
||||
default: ''
|
||||
italics:
|
||||
description: 'Description *with* italics'
|
||||
type: 'string'
|
||||
default: ''
|
||||
bold:
|
||||
description: 'Description **with** bold'
|
||||
type: 'string'
|
||||
default: ''
|
||||
link:
|
||||
description: 'Description [with](http://www.example.com) link'
|
||||
type: 'string'
|
||||
default: ''
|
||||
inlineCode:
|
||||
description: 'Description `with` inline code'
|
||||
type: 'string'
|
||||
default: ''
|
||||
lineBreak:
|
||||
description: 'Description with<br/> line break'
|
||||
type: 'string'
|
||||
default: ''
|
||||
strikethrough:
|
||||
description: 'Description ~~with~~ strikethrough'
|
||||
type: 'string'
|
||||
default: ''
|
||||
image:
|
||||
description: 'Description without ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1") image'
|
||||
type: 'string'
|
||||
default: ''
|
||||
fencedBlockCode:
|
||||
description: '''Description without fenced block code
|
||||
```
|
||||
Test
|
||||
```
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
indentedBlockCode:
|
||||
description: '''
|
||||
Description without indented block code
|
||||
|
||||
Test
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
blockquote:
|
||||
description: '''
|
||||
Description without blockquote
|
||||
|
||||
> Test
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
html:
|
||||
description: '''
|
||||
Description without html
|
||||
|
||||
<html>Test</html>
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
heading:
|
||||
description: '''
|
||||
Description without heading
|
||||
|
||||
## Test
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
orderedList:
|
||||
description: '''
|
||||
Description without ordered list
|
||||
|
||||
1. Test
|
||||
2. Test
|
||||
3. Test
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
unorderedList:
|
||||
description: '''
|
||||
Description without unordered list
|
||||
|
||||
* Test
|
||||
* Test
|
||||
* Test
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
table:
|
||||
description: '''
|
||||
Description without table
|
||||
|
||||
<table><tr><td>Test</td></tr></table>
|
||||
'''
|
||||
type: 'string'
|
||||
default: ''
|
||||
|
||||
atom.config.setSchema("foo", config)
|
||||
|
||||
describe 'supported Markdown', ->
|
||||
it 'handles plain text', ->
|
||||
expect(getSettingDescription('foo.plainText')).toEqual 'Plain text description'
|
||||
|
||||
it 'handles italics', ->
|
||||
expect(getSettingDescription('foo.italics')).toEqual 'Description <em>with</em> italics'
|
||||
|
||||
it 'handles bold', ->
|
||||
expect(getSettingDescription('foo.bold')).toEqual 'Description <strong>with</strong> bold'
|
||||
|
||||
it 'handles links', ->
|
||||
expect(getSettingDescription('foo.link')).toEqual 'Description <a href="http://www.example.com">with</a> link'
|
||||
|
||||
it 'handles inline code', ->
|
||||
expect(getSettingDescription('foo.inlineCode')).toEqual 'Description <code>with</code> inline code'
|
||||
|
||||
it 'handles line breaks', ->
|
||||
expect(getSettingDescription('foo.lineBreak')).toEqual 'Description with<br/> line break'
|
||||
|
||||
it 'handles strikethrough', ->
|
||||
expect(getSettingDescription('foo.strikethrough')).toEqual 'Description <del>with</del> strikethrough'
|
||||
|
||||
describe 'unsupported Markdown', ->
|
||||
it 'strips images', ->
|
||||
expect(getSettingDescription('foo.image')).toEqual 'Description without image'
|
||||
|
||||
it 'strips fenced code blocks', ->
|
||||
expect(getSettingDescription('foo.fencedBlockCode')).toEqual 'Description without fenced block code'
|
||||
|
||||
it 'strips indented code blocks', ->
|
||||
expect(getSettingDescription('foo.indentedBlockCode')).toEqual 'Description without indented block code'
|
||||
|
||||
it 'strips blockquotes', ->
|
||||
expect(getSettingDescription('foo.blockquote')).toEqual 'Description without blockquote'
|
||||
|
||||
it 'strips html elements', ->
|
||||
expect(getSettingDescription('foo.html')).toEqual 'Description without html'
|
||||
|
||||
it 'strips headings', ->
|
||||
expect(getSettingDescription('foo.heading')).toEqual 'Description without heading'
|
||||
|
||||
it 'strips ordered lists', ->
|
||||
expect(getSettingDescription('foo.orderedList')).toEqual 'Description without ordered list'
|
||||
|
||||
it 'strips unordered lists', ->
|
||||
expect(getSettingDescription('foo.unorderedList')).toEqual 'Description without unordered list'
|
||||
|
||||
it 'strips tables', ->
|
||||
expect(getSettingDescription('foo.table')).toEqual 'Description without table'
|
381
packages/settings-view/spec/settings-panel-spec.coffee
Normal file
381
packages/settings-view/spec/settings-panel-spec.coffee
Normal file
@ -0,0 +1,381 @@
|
||||
SettingsPanel = require '../lib/settings-panel'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
describe "SettingsPanel", ->
|
||||
settingsPanel = null
|
||||
|
||||
describe "sorted settings", ->
|
||||
beforeEach ->
|
||||
config =
|
||||
type: 'object'
|
||||
properties:
|
||||
bar:
|
||||
title: 'Bar'
|
||||
description: 'The bar setting'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
haz:
|
||||
title: 'Haz'
|
||||
description: 'The haz setting'
|
||||
type: 'string'
|
||||
default: 'haz'
|
||||
zing:
|
||||
title: 'Zing'
|
||||
description: 'The zing setting'
|
||||
type: 'string'
|
||||
default: 'zing'
|
||||
order: 1
|
||||
zang:
|
||||
title: 'Zang'
|
||||
description: 'The baz setting'
|
||||
type: 'string'
|
||||
default: 'zang'
|
||||
order: 100
|
||||
enum:
|
||||
title: 'An enum'
|
||||
type: 'string'
|
||||
default: 'one'
|
||||
enum: [
|
||||
{value: 'one', description: 'One'}
|
||||
'Two'
|
||||
]
|
||||
radio:
|
||||
title: 'An enum with radio buttons'
|
||||
radio: true
|
||||
type: 'string'
|
||||
default: 'Two'
|
||||
enum: [
|
||||
{value: 'one', description: 'One'}
|
||||
'Two'
|
||||
]
|
||||
atom.config.setSchema("foo", config)
|
||||
atom.config.setDefaults("foo", gong: 'gong')
|
||||
expect(_.size(atom.config.get('foo'))).toBe 7
|
||||
settingsPanel = new SettingsPanel({namespace: "foo", includeTitle: false})
|
||||
|
||||
it "sorts settings by order and then alphabetically by the key", ->
|
||||
settings = atom.config.get('foo')
|
||||
expect(_.size(settings)).toBe 7
|
||||
sortedSettings = settingsPanel.sortSettings("foo", settings)
|
||||
expect(sortedSettings[0]).toBe 'zing'
|
||||
expect(sortedSettings[1]).toBe 'zang'
|
||||
expect(sortedSettings[2]).toBe 'bar'
|
||||
expect(sortedSettings[3]).toBe 'enum'
|
||||
expect(sortedSettings[4]).toBe 'gong'
|
||||
expect(sortedSettings[5]).toBe 'haz'
|
||||
expect(sortedSettings[6]).toBe 'radio'
|
||||
|
||||
it "gracefully deals with a null settings object", ->
|
||||
sortedSettings = settingsPanel.sortSettings("foo", null)
|
||||
expect(sortedSettings).not.toBeNull
|
||||
expect(_.size(sortedSettings)).toBe 0
|
||||
|
||||
it "presents enum options with their descriptions", ->
|
||||
select = settingsPanel.element.querySelector('#foo\\.enum')
|
||||
pairs = ([opt.value, opt.innerText] for opt in select.children)
|
||||
expect(pairs).toEqual([['one', 'One'], ['Two', 'Two']])
|
||||
|
||||
it "presents radio options with their descriptions", ->
|
||||
radio = settingsPanel.element.querySelector('#foo\\.radio')
|
||||
options = for label in radio.querySelectorAll 'label'
|
||||
button = label.querySelector('input[type=radio][name="foo.radio"]')
|
||||
[button.id, button.value, label.innerText]
|
||||
expect(options).toEqual([['foo.radio[one]', 'one', 'One'], ['foo.radio[Two]', 'Two', 'Two']])
|
||||
|
||||
describe 'default settings', ->
|
||||
beforeEach ->
|
||||
config =
|
||||
type: 'object'
|
||||
properties:
|
||||
haz:
|
||||
name: 'haz'
|
||||
title: 'Haz'
|
||||
description: 'The haz setting'
|
||||
type: 'string'
|
||||
default: 'haz'
|
||||
qux:
|
||||
name: 'qux'
|
||||
title: 'Qux'
|
||||
description: 'The qux setting'
|
||||
type: 'string'
|
||||
default: 'a'
|
||||
enum: [
|
||||
{value: 'a', description: 'Alice'},
|
||||
{value: 'b', description: 'Bob'}
|
||||
]
|
||||
testZero:
|
||||
name: 'testZero'
|
||||
title: 'Test Zero'
|
||||
description: 'Setting for testing zero as a default'
|
||||
type: 'integer'
|
||||
default: 0
|
||||
radio:
|
||||
title: 'An enum with radio buttons'
|
||||
radio: true
|
||||
type: 'string'
|
||||
default: 'Two'
|
||||
enum: [
|
||||
{value: 'one', description: 'One'}
|
||||
'Two'
|
||||
'Three'
|
||||
]
|
||||
atom.config.setSchema("foo", config)
|
||||
atom.config.setDefaults("foo", gong: 'gong')
|
||||
expect(_.size(atom.config.get('foo'))).toBe 5
|
||||
settingsPanel = new SettingsPanel({namespace: "foo", includeTitle: false})
|
||||
|
||||
it 'ensures default stays default', ->
|
||||
expect(settingsPanel.getDefault('foo.haz')).toBe 'haz'
|
||||
expect(settingsPanel.isDefault('foo.haz')).toBe true
|
||||
settingsPanel.set('foo.haz', 'haz')
|
||||
expect(settingsPanel.isDefault('foo.haz')).toBe true
|
||||
|
||||
it 'can be overwritten', ->
|
||||
expect(settingsPanel.getDefault('foo.haz')).toBe 'haz'
|
||||
expect(settingsPanel.isDefault('foo.haz')).toBe true
|
||||
settingsPanel.set('foo.haz', 'newhaz')
|
||||
expect(settingsPanel.isDefault('foo.haz')).toBe false
|
||||
expect(atom.config.get('foo.haz')).toBe 'newhaz'
|
||||
|
||||
it 'has a tooltip showing the default value', ->
|
||||
hazEditor = settingsPanel.element.querySelector('[id="foo.haz"]')
|
||||
tooltips = atom.tooltips.findTooltips(hazEditor)
|
||||
expect(tooltips).toHaveLength 1
|
||||
title = tooltips[0].options.title
|
||||
expect(title).toBe "Default: haz"
|
||||
|
||||
it 'has a tooltip showing the description of the default value', ->
|
||||
quxEditor = settingsPanel.element.querySelector('[id="foo.qux"]')
|
||||
tooltips = atom.tooltips.findTooltips(quxEditor)
|
||||
expect(tooltips).toHaveLength 1
|
||||
title = tooltips[0].options.title
|
||||
expect(title).toBe "Default: Alice"
|
||||
|
||||
# Regression test for #783
|
||||
it 'allows 0 to be a default', ->
|
||||
zeroEditor = settingsPanel.element.querySelector('[id="foo.testZero"]')
|
||||
expect(zeroEditor.getModel().getText()).toBe('')
|
||||
expect(zeroEditor.getModel().getPlaceholderText()).toBe('Default: 0')
|
||||
|
||||
expect(settingsPanel.getDefault('foo.testZero')).toBe 0
|
||||
expect(settingsPanel.isDefault('foo.testZero')).toBe true
|
||||
|
||||
settingsPanel.set('foo.testZero', 15)
|
||||
expect(settingsPanel.isDefault('foo.testZero')).toBe false
|
||||
|
||||
settingsPanel.set('foo.testZero', 0)
|
||||
expect(settingsPanel.isDefault('foo.testZero')).toBe true
|
||||
|
||||
it "selects the default choice for radio options", ->
|
||||
expect(settingsPanel.getDefault 'foo.radio').toBe 'Two'
|
||||
settingsPanel.set 'foo.radio', 'Two'
|
||||
expect(settingsPanel.element.querySelector '#foo\\.radio\\[Two\\]').toBeChecked()
|
||||
|
||||
describe 'scoped settings', ->
|
||||
beforeEach ->
|
||||
schema =
|
||||
scopes:
|
||||
'.source.python':
|
||||
default: 4
|
||||
|
||||
atom.config.setScopedDefaultsFromSchema('editor.tabLength', schema)
|
||||
expect(atom.config.get('editor.tabLength')).toBe(2)
|
||||
|
||||
it 'displays the scoped default', ->
|
||||
settingsPanel = new SettingsPanel({namespace: "editor", includeTitle: false, scopeName: '.source.python'})
|
||||
tabLengthEditor = settingsPanel.element.querySelector('[id="editor.tabLength"]')
|
||||
expect(tabLengthEditor.getModel().getText()).toBe('')
|
||||
expect(tabLengthEditor.getModel().getPlaceholderText()).toBe('Default: 4')
|
||||
|
||||
it 'allows the scoped setting to be changed to its normal default if the unscoped value is different', ->
|
||||
atom.config.set('editor.tabLength', 8)
|
||||
|
||||
settingsPanel = new SettingsPanel({namespace: "editor", includeTitle: false, scopeName: '.source.js'})
|
||||
tabLengthEditor = settingsPanel.element.querySelector('[id="editor.tabLength"]')
|
||||
expect(tabLengthEditor.getModel().getText()).toBe('')
|
||||
expect(tabLengthEditor.getModel().getPlaceholderText()).toBe('Default: 8')
|
||||
|
||||
# This is the unscoped default, but it differs from the current unscoped value
|
||||
settingsPanel.set('editor.tabLength', 2)
|
||||
expect(tabLengthEditor.getModel().getText()).toBe('2')
|
||||
expect(atom.config.get('editor.tabLength', {scope: ['source.js']})).toBe(2)
|
||||
|
||||
it 'allows the scoped setting to be changed to the unscoped default if it is different', ->
|
||||
settingsPanel = new SettingsPanel({namespace: "editor", includeTitle: false, scopeName: '.source.python'})
|
||||
tabLengthEditor = settingsPanel.element.querySelector('[id="editor.tabLength"]')
|
||||
expect(tabLengthEditor.getModel().getText()).toBe('')
|
||||
expect(tabLengthEditor.getModel().getPlaceholderText()).toBe('Default: 4')
|
||||
|
||||
# This is the unscoped default, but it differs from the scoped default
|
||||
settingsPanel.set('editor.tabLength', 2)
|
||||
expect(tabLengthEditor.getModel().getText()).toBe('2')
|
||||
expect(atom.config.get('editor.tabLength', {scope: ['source.python']})).toBe(2)
|
||||
|
||||
describe 'grouped settings', ->
|
||||
beforeEach ->
|
||||
config =
|
||||
type: 'object'
|
||||
properties:
|
||||
barGroup:
|
||||
type: 'object'
|
||||
title: 'Bar group'
|
||||
description: 'description of bar group'
|
||||
properties:
|
||||
bar:
|
||||
title: 'Bar'
|
||||
description: 'The bar setting'
|
||||
type: 'boolean'
|
||||
default: false
|
||||
bazGroup:
|
||||
type: 'object'
|
||||
collapsed: true
|
||||
properties:
|
||||
baz:
|
||||
title: 'Baz'
|
||||
description: 'The baz setting'
|
||||
type: 'boolean'
|
||||
default: false
|
||||
zing:
|
||||
type: 'string'
|
||||
default: ''
|
||||
atom.config.setSchema('foo', config)
|
||||
expect(_.size(atom.config.get('foo'))).toBe 3
|
||||
settingsPanel = new SettingsPanel({namespace: 'foo', includeTitle: false})
|
||||
|
||||
it 'ensures that only grouped settings have a group title', ->
|
||||
expect(settingsPanel.element.querySelectorAll('.section-container > .section-body')).toHaveLength 1
|
||||
controlGroups = settingsPanel.element.querySelectorAll('.section-body > .control-group')
|
||||
expect(controlGroups).toHaveLength 3
|
||||
expect(controlGroups[0].querySelectorAll('.sub-section .sub-section-heading')).toHaveLength 1
|
||||
expect(controlGroups[0].querySelector('.sub-section .sub-section-heading').textContent).toBe 'Bar group'
|
||||
expect(controlGroups[0].querySelectorAll('.sub-section .sub-section-body')).toHaveLength 1
|
||||
subsectionBody = controlGroups[0].querySelector('.sub-section .sub-section-body')
|
||||
expect(subsectionBody.querySelectorAll('.control-group')).toHaveLength 1
|
||||
expect(controlGroups[1].querySelectorAll('.sub-section .sub-section-heading')).toHaveLength 1
|
||||
expect(controlGroups[1].querySelector('.sub-section .sub-section-heading').textContent).toBe 'Baz Group'
|
||||
expect(controlGroups[1].querySelectorAll('.sub-section .sub-section-body')).toHaveLength 1
|
||||
subsectionBody = controlGroups[1].querySelector('.sub-section .sub-section-body')
|
||||
expect(subsectionBody.querySelectorAll('.control-group')).toHaveLength 1
|
||||
expect(controlGroups[2].querySelectorAll('.sub-section')).toHaveLength 0
|
||||
expect(controlGroups[2].querySelectorAll('.sub-section-heading')).toHaveLength 0
|
||||
|
||||
it 'ensures grouped settings are collapsable', ->
|
||||
expect(settingsPanel.element.querySelectorAll('.section-container > .section-body')).toHaveLength 1
|
||||
controlGroups = settingsPanel.element.querySelectorAll('.section-body > .control-group')
|
||||
expect(controlGroups).toHaveLength 3
|
||||
# Bar group
|
||||
expect(controlGroups[0].querySelectorAll('.sub-section .sub-section-heading')).toHaveLength 1
|
||||
expect(controlGroups[0].querySelector('.sub-section .sub-section-heading').classList.contains('has-items')).toBe true
|
||||
# Baz Group
|
||||
expect(controlGroups[1].querySelectorAll('.sub-section .sub-section-heading')).toHaveLength 1
|
||||
expect(controlGroups[1].querySelector('.sub-section .sub-section-heading').classList.contains('has-items')).toBe true
|
||||
# Should be already collapsed
|
||||
expect(controlGroups[1].querySelector('.sub-section .sub-section-heading').parentElement.classList.contains('collapsed')).toBe true
|
||||
|
||||
it 'ensures grouped settings can have a description', ->
|
||||
expect(settingsPanel.element.querySelectorAll('.section-container > .section-body')).toHaveLength 1
|
||||
controlGroups = settingsPanel.element.querySelectorAll('.section-body > .control-group')
|
||||
expect(controlGroups).toHaveLength 3
|
||||
expect(controlGroups[0].querySelectorAll('.sub-section > .setting-description')).toHaveLength 1
|
||||
expect(controlGroups[0].querySelector('.sub-section > .setting-description').textContent).toBe 'description of bar group'
|
||||
|
||||
describe 'settings validation', ->
|
||||
beforeEach ->
|
||||
config =
|
||||
type: 'object'
|
||||
properties:
|
||||
minMax:
|
||||
name: 'minMax'
|
||||
title: 'Min max'
|
||||
description: 'The minMax setting'
|
||||
type: 'integer'
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
commaValueArray:
|
||||
name: 'commaValueArray'
|
||||
title: 'Comma value in array'
|
||||
description: 'An array with a comma value'
|
||||
type: 'array'
|
||||
default: []
|
||||
|
||||
atom.config.setSchema('foo', config)
|
||||
settingsPanel = new SettingsPanel({namespace: 'foo', includeTitle: false})
|
||||
|
||||
it 'prevents setting a value below the minimum', ->
|
||||
minMaxEditor = settingsPanel.element.querySelector('[id="foo.minMax"]')
|
||||
minMaxEditor.getModel().setText('0')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '1'
|
||||
|
||||
minMaxEditor.getModel().setText('-5')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '1'
|
||||
|
||||
it 'prevents setting a value above the maximum', ->
|
||||
minMaxEditor = settingsPanel.element.querySelector('[id="foo.minMax"]')
|
||||
minMaxEditor.getModel().setText('1000')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '100'
|
||||
|
||||
minMaxEditor.getModel().setText('10000')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '100'
|
||||
|
||||
it 'prevents setting a value that cannot be coerced to the correct type', ->
|
||||
minMaxEditor = settingsPanel.element.querySelector('[id="foo.minMax"]')
|
||||
minMaxEditor.getModel().setText('"abcde"')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '' # aka default
|
||||
|
||||
minMaxEditor.getModel().setText('15')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '15'
|
||||
|
||||
minMaxEditor.getModel().setText('"abcde"')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '15'
|
||||
|
||||
it 'allows setting a valid scoped value', ->
|
||||
settingsPanel = new SettingsPanel({namespace: 'foo', includeTitle: false, scopeName: 'source.js'})
|
||||
minMaxEditor = settingsPanel.element.querySelector('atom-text-editor')
|
||||
minMaxEditor.getModel().setText('15')
|
||||
advanceClock(minMaxEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(minMaxEditor.getModel().getText()).toBe '15'
|
||||
|
||||
describe 'commaValueArray', ->
|
||||
it 'comma in value is escaped', ->
|
||||
commaValueArrayEditor = settingsPanel.element.querySelector('[id="foo.commaValueArray"]')
|
||||
commaValueArrayEditor.getModel().setText('1, \\,, 2')
|
||||
advanceClock(commaValueArrayEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(atom.config.get("foo.commaValueArray")).toEqual ['1', ',', '2']
|
||||
|
||||
commaValueArrayEditor.getModel().setText('1\\, 2')
|
||||
advanceClock(commaValueArrayEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(atom.config.get('foo.commaValueArray')).toEqual ['1, 2']
|
||||
|
||||
commaValueArrayEditor.getModel().setText('1\\,')
|
||||
advanceClock(commaValueArrayEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(atom.config.get('foo.commaValueArray')).toEqual ['1,']
|
||||
|
||||
commaValueArrayEditor.getModel().setText('\\, 2')
|
||||
advanceClock(commaValueArrayEditor.getModel().getBuffer().getStoppedChangingDelay())
|
||||
expect(atom.config.get('foo.commaValueArray')).toEqual [', 2']
|
||||
|
||||
it 'renders an escaped comma', ->
|
||||
commaValueArrayEditor = settingsPanel.element.querySelector('[id="foo.commaValueArray"]')
|
||||
atom.config.set('foo.commaValueArray', ['3', ',', '4'])
|
||||
advanceClock(1000)
|
||||
expect(commaValueArrayEditor.getModel().getText()).toBe '3, \\,, 4'
|
||||
|
||||
atom.config.set('foo.commaValueArray', ['3, 4'])
|
||||
advanceClock(1000)
|
||||
expect(commaValueArrayEditor.getModel().getText()).toBe '3\\, 4'
|
||||
|
||||
atom.config.set('foo.commaValueArray', ['3,'])
|
||||
advanceClock(1000)
|
||||
expect(commaValueArrayEditor.getModel().getText()).toBe '3\\,'
|
||||
|
||||
atom.config.set('foo.commaValueArray', [', 4'])
|
||||
advanceClock(1000)
|
||||
expect(commaValueArrayEditor.getModel().getText()).toBe '\\, 4'
|
519
packages/settings-view/spec/settings-view-spec.coffee
Normal file
519
packages/settings-view/spec/settings-view-spec.coffee
Normal file
@ -0,0 +1,519 @@
|
||||
path = require 'path'
|
||||
main = require '../lib/main'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
SnippetsProvider =
|
||||
getSnippets: -> {}
|
||||
|
||||
describe "SettingsView", ->
|
||||
settingsView = null
|
||||
packageManager = new PackageManager()
|
||||
|
||||
beforeEach ->
|
||||
settingsView = main.createSettingsView({packageManager: packageManager, snippetsProvider: SnippetsProvider})
|
||||
spyOn(settingsView, "initializePanels").andCallThrough()
|
||||
window.advanceClock(10000)
|
||||
waitsFor ->
|
||||
settingsView.initializePanels.callCount > 0
|
||||
|
||||
describe "serialization", ->
|
||||
it "remembers which panel was visible", ->
|
||||
settingsView.showPanel('Themes')
|
||||
newSettingsView = main.createSettingsView(settingsView.serialize())
|
||||
settingsView.destroy()
|
||||
jasmine.attachToDOM(newSettingsView.element)
|
||||
newSettingsView.initializePanels()
|
||||
expect(newSettingsView.activePanel).toEqual {name: 'Themes', options: {}}
|
||||
|
||||
it "shows the previously active panel if it is added after deserialization", ->
|
||||
settingsView.addCorePanel('Panel 1', 'panel-1', ->
|
||||
div = document.createElement('div')
|
||||
div.id = 'panel-1'
|
||||
{
|
||||
element: div,
|
||||
show: -> div.style.display = '',
|
||||
focus: -> div.focus(),
|
||||
destroy: -> div.remove()
|
||||
}
|
||||
)
|
||||
settingsView.showPanel('Panel 1')
|
||||
newSettingsView = main.createSettingsView(settingsView.serialize())
|
||||
newSettingsView.addPanel('Panel 1', ->
|
||||
div = document.createElement('div')
|
||||
div.id = 'panel-1'
|
||||
{
|
||||
element: div,
|
||||
show: -> div.style.display = '',
|
||||
focus: -> div.focus(),
|
||||
destroy: -> div.remove()
|
||||
}
|
||||
)
|
||||
newSettingsView.initializePanels()
|
||||
jasmine.attachToDOM(newSettingsView.element)
|
||||
expect(newSettingsView.activePanel).toEqual {name: 'Panel 1', options: {}}
|
||||
|
||||
it "shows the Settings panel if the last saved active panel name no longer exists", ->
|
||||
settingsView.addCorePanel('Panel 1', 'panel1', ->
|
||||
div = document.createElement('div')
|
||||
div.id = 'panel-1'
|
||||
{
|
||||
element: div,
|
||||
show: -> div.style.display = '',
|
||||
focus: -> div.focus(),
|
||||
destroy: -> div.remove()
|
||||
}
|
||||
)
|
||||
settingsView.showPanel('Panel 1')
|
||||
newSettingsView = main.createSettingsView(settingsView.serialize())
|
||||
settingsView.destroy()
|
||||
jasmine.attachToDOM(newSettingsView.element)
|
||||
newSettingsView.initializePanels()
|
||||
expect(newSettingsView.activePanel).toEqual {name: 'Core', options: {}}
|
||||
|
||||
it "serializes the active panel name even when the panels were never initialized", ->
|
||||
settingsView.showPanel('Themes')
|
||||
settingsView2 = main.createSettingsView(settingsView.serialize())
|
||||
settingsView3 = main.createSettingsView(settingsView2.serialize())
|
||||
jasmine.attachToDOM(settingsView3.element)
|
||||
settingsView3.initializePanels()
|
||||
expect(settingsView3.activePanel).toEqual {name: 'Themes', options: {}}
|
||||
|
||||
describe ".addCorePanel(name, iconName, view)", ->
|
||||
it "adds a menu entry to the left and a panel that can be activated by clicking it", ->
|
||||
settingsView.addCorePanel('Panel 1', 'panel1', ->
|
||||
div = document.createElement('div')
|
||||
div.id = 'panel-1'
|
||||
{
|
||||
element: div,
|
||||
show: -> div.style.display = '',
|
||||
focus: -> div.focus(),
|
||||
destroy: -> div.remove()
|
||||
}
|
||||
)
|
||||
settingsView.addCorePanel('Panel 2', 'panel2', ->
|
||||
div = document.createElement('div')
|
||||
div.id = 'panel-2'
|
||||
{
|
||||
element: div,
|
||||
show: -> div.style.display = '',
|
||||
focus: -> div.focus(),
|
||||
destroy: -> div.remove()
|
||||
}
|
||||
)
|
||||
|
||||
expect(settingsView.refs.panelMenu.querySelector('li[name="Panel 1"]')).toExist()
|
||||
expect(settingsView.refs.panelMenu.querySelector('li[name="Panel 2"]')).toExist()
|
||||
expect(settingsView.refs.panelMenu.children[0]).toHaveClass 'active'
|
||||
|
||||
jasmine.attachToDOM(settingsView.element)
|
||||
settingsView.refs.panelMenu.querySelector('li[name="Panel 1"] a').click()
|
||||
expect(settingsView.refs.panelMenu.querySelectorAll('.active').length).toBe 1
|
||||
expect(settingsView.refs.panelMenu.querySelector('li[name="Panel 1"]')).toHaveClass('active')
|
||||
expect(settingsView.refs.panels.querySelector('#panel-1')).toBeVisible()
|
||||
expect(settingsView.refs.panels.querySelector('#panel-2')).not.toExist()
|
||||
settingsView.refs.panelMenu.querySelector('li[name="Panel 2"] a').click()
|
||||
expect(settingsView.refs.panelMenu.querySelectorAll('.active').length).toBe 1
|
||||
expect(settingsView.refs.panelMenu.querySelector('li[name="Panel 2"]')).toHaveClass('active')
|
||||
expect(settingsView.refs.panels.querySelector('#panel-1')).toBeHidden()
|
||||
expect(settingsView.refs.panels.querySelector('#panel-2')).toBeVisible()
|
||||
|
||||
describe "when the package is activated", ->
|
||||
openWithCommand = (command) ->
|
||||
waitsFor (done) ->
|
||||
openSubscription = atom.workspace.onDidOpen ->
|
||||
openSubscription.dispose()
|
||||
done()
|
||||
atom.commands.dispatch(atom.views.getView(atom.workspace), command)
|
||||
|
||||
beforeEach ->
|
||||
jasmine.attachToDOM(atom.views.getView(atom.workspace))
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('settings-view')
|
||||
|
||||
describe "when the settings view is opened with a settings-view:* command", ->
|
||||
beforeEach ->
|
||||
settingsView = null
|
||||
|
||||
describe "settings-view:open", ->
|
||||
it "opens the settings view", ->
|
||||
openWithCommand('settings-view:open')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Core', options: {}
|
||||
|
||||
it "always open existing item in workspace", ->
|
||||
center = atom.workspace.getCenter()
|
||||
[pane1, pane2] = []
|
||||
|
||||
waitsForPromise -> atom.workspace.open(null, split: 'right')
|
||||
runs ->
|
||||
expect(center.getPanes()).toHaveLength(2)
|
||||
[pane1, pane2] = center.getPanes()
|
||||
expect(atom.workspace.getActivePane()).toBe(pane2)
|
||||
|
||||
openWithCommand('settings-view:open')
|
||||
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel).toEqual name: 'Core', options: {}
|
||||
expect(atom.workspace.getActivePane()).toBe(pane2)
|
||||
|
||||
runs ->
|
||||
pane1.activate()
|
||||
|
||||
openWithCommand('settings-view:open')
|
||||
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel).toEqual name: 'Core', options: {}
|
||||
expect(atom.workspace.getActivePane()).toBe(pane2)
|
||||
|
||||
describe "settings-view:core", ->
|
||||
it "opens the core settings view", ->
|
||||
openWithCommand('settings-view:editor')
|
||||
runs ->
|
||||
openWithCommand('settings-view:core')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Core', options: uri: 'atom://config/core'
|
||||
|
||||
describe "settings-view:editor", ->
|
||||
it "opens the editor settings view", ->
|
||||
openWithCommand('settings-view:editor')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Editor', options: uri: 'atom://config/editor'
|
||||
|
||||
describe "settings-view:show-keybindings", ->
|
||||
it "opens the settings view to the keybindings page", ->
|
||||
openWithCommand('settings-view:show-keybindings')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Keybindings', options: uri: 'atom://config/keybindings'
|
||||
|
||||
describe "settings-view:change-themes", ->
|
||||
it "opens the settings view to the themes page", ->
|
||||
openWithCommand('settings-view:change-themes')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Themes', options: uri: 'atom://config/themes'
|
||||
|
||||
describe "settings-view:uninstall-themes", ->
|
||||
it "opens the settings view to the themes page", ->
|
||||
openWithCommand('settings-view:uninstall-themes')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Themes', options: uri: 'atom://config/themes'
|
||||
|
||||
describe "settings-view:uninstall-packages", ->
|
||||
it "opens the settings view to the install page", ->
|
||||
openWithCommand('settings-view:uninstall-packages')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Packages', options: uri: 'atom://config/packages'
|
||||
|
||||
describe "settings-view:install-packages-and-themes", ->
|
||||
it "opens the settings view to the install page", ->
|
||||
openWithCommand('settings-view:install-packages-and-themes')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Install', options: uri: 'atom://config/install'
|
||||
|
||||
describe "settings-view:check-for-package-updates", ->
|
||||
it "opens the settings view to the install page", ->
|
||||
openWithCommand('settings-view:check-for-package-updates')
|
||||
runs ->
|
||||
expect(atom.workspace.getActivePaneItem().activePanel)
|
||||
.toEqual name: 'Updates', options: uri: 'atom://config/updates'
|
||||
|
||||
describe "when atom.workspace.open() is used with a config URI", ->
|
||||
focusIsWithinActivePanel = ->
|
||||
activePanel = settingsView.panelsByName[settingsView.activePanel.name]
|
||||
activePanel.element is document.activeElement or activePanel.element.contains(document.activeElement)
|
||||
|
||||
expectActivePanelToBeKeyboardScrollable = ->
|
||||
activePanel = settingsView.panelsByName[settingsView.activePanel.name]
|
||||
spyOn(activePanel, 'pageDown')
|
||||
atom.commands.dispatch(activePanel.element, 'core:page-down')
|
||||
expect(activePanel.pageDown).toHaveBeenCalled()
|
||||
spyOn(activePanel, 'pageUp')
|
||||
atom.commands.dispatch(activePanel.element, 'core:page-up')
|
||||
expect(activePanel.pageUp).toHaveBeenCalled()
|
||||
|
||||
beforeEach ->
|
||||
settingsView = null
|
||||
|
||||
it "opens the settings to the correct panel with atom://config/<panel-name> and that panel is keyboard-scrollable", ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config').then (s) -> settingsView = s
|
||||
|
||||
waitsFor (done) -> process.nextTick(done)
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Core', options: {}
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/editor').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Editor', options: uri: 'atom://config/editor'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/keybindings').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Keybindings', options: uri: 'atom://config/keybindings'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/packages').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Packages', options: uri: 'atom://config/packages'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/themes').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Themes', options: uri: 'atom://config/themes'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/updates').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Updates', options: uri: 'atom://config/updates'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/install').then (s) -> settingsView = s
|
||||
|
||||
hasSystemPanel = false
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Install', options: uri: 'atom://config/install'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
hasSystemPanel = settingsView.panelsByName['System']?
|
||||
|
||||
if hasSystemPanel
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/system').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'System', options: uri: 'atom://config/system'
|
||||
expect(focusIsWithinActivePanel()).toBe true
|
||||
expectActivePanelToBeKeyboardScrollable()
|
||||
|
||||
it "opens the package settings view with atom://config/packages/<package-name>", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/packages/package-with-readme').then (s) -> settingsView = s
|
||||
|
||||
waitsFor (done) -> process.nextTick(done)
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'package-with-readme', options: {
|
||||
uri: 'atom://config/packages/package-with-readme',
|
||||
pack:
|
||||
name: 'package-with-readme'
|
||||
metadata:
|
||||
name: 'package-with-readme'
|
||||
back: 'Packages'
|
||||
}
|
||||
|
||||
it "doesn't use cached package detail when package re-activated and opnes the package view with atom://config/packages/<package-name>", ->
|
||||
[detailInitial, detailAfterReactivate] = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activate()
|
||||
new Promise (resolve) -> atom.packages.onDidActivateInitialPackages(resolve)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/packages/package-with-readme').then (s) -> settingsView = s
|
||||
|
||||
waitsFor (done) -> process.nextTick(done)
|
||||
|
||||
runs ->
|
||||
detailInitial = settingsView.getOrCreatePanel('package-with-readme')
|
||||
expect(settingsView.getOrCreatePanel('package-with-readme')).toBe detailInitial
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.deactivatePackage('package-with-readme')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/packages/package-with-readme')
|
||||
|
||||
runs ->
|
||||
detailAfterReactivate = settingsView.getOrCreatePanel('package-with-readme')
|
||||
expect(settingsView.getOrCreatePanel('package-with-readme')).toBe detailAfterReactivate
|
||||
expect(detailInitial).toBeTruthy()
|
||||
expect(detailAfterReactivate).toBeTruthy()
|
||||
expect(detailInitial).not.toBe(detailAfterReactivate)
|
||||
|
||||
it "passes the URI to a pane's beforeShow() method on settings view initialization", ->
|
||||
InstallPanel = require '../lib/install-panel'
|
||||
spyOn(InstallPanel::, 'beforeShow')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/install/package:something').then (s) -> settingsView = s
|
||||
|
||||
waitsFor ->
|
||||
settingsView.activePanel?
|
||||
, 'The activePanel should be set', 5000
|
||||
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Install', options: uri: 'atom://config/install/package:something'
|
||||
expect(InstallPanel::beforeShow).toHaveBeenCalledWith {uri: 'atom://config/install/package:something'}
|
||||
|
||||
it "passes the URI to a pane's beforeShow() method after initialization", ->
|
||||
InstallPanel = require '../lib/install-panel'
|
||||
spyOn(InstallPanel::, 'beforeShow')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config').then (s) -> settingsView = s
|
||||
|
||||
waitsFor (done) -> process.nextTick(done)
|
||||
|
||||
runs ->
|
||||
expect(settingsView.activePanel).toEqual {name: 'Core', options: {}}
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('atom://config/install/package:something').then (s) -> settingsView = s
|
||||
|
||||
waits 1
|
||||
runs ->
|
||||
expect(settingsView.activePanel)
|
||||
.toEqual name: 'Install', options: uri: 'atom://config/install/package:something'
|
||||
expect(InstallPanel::beforeShow).toHaveBeenCalledWith {uri: 'atom://config/install/package:something'}
|
||||
|
||||
describe "when the package is then deactivated", ->
|
||||
beforeEach ->
|
||||
settingsView = null
|
||||
|
||||
it "calls the dispose method on all panels", ->
|
||||
openWithCommand('settings-view:open')
|
||||
|
||||
waitsFor (done) -> process.nextTick(done)
|
||||
|
||||
runs ->
|
||||
settingsView = atom.workspace.getActivePaneItem()
|
||||
panels = [
|
||||
settingsView.getOrCreatePanel('Core')
|
||||
settingsView.getOrCreatePanel('Editor')
|
||||
settingsView.getOrCreatePanel('Keybindings')
|
||||
settingsView.getOrCreatePanel('Packages')
|
||||
settingsView.getOrCreatePanel('Themes')
|
||||
settingsView.getOrCreatePanel('Updates')
|
||||
settingsView.getOrCreatePanel('Install')
|
||||
]
|
||||
systemPanel = settingsView.getOrCreatePanel('System')
|
||||
if systemPanel?
|
||||
panels.push systemPanel
|
||||
for panel in panels
|
||||
if panel.dispose
|
||||
spyOn(panel, 'dispose')
|
||||
else
|
||||
spyOn(panel, 'destroy')
|
||||
|
||||
waitsForPromise ->
|
||||
Promise.resolve(atom.packages.deactivatePackage('settings-view')) # Ensure works on promise and non-promise versions
|
||||
|
||||
runs ->
|
||||
for panel in panels
|
||||
if panel.dispose
|
||||
expect(panel.dispose).toHaveBeenCalled()
|
||||
else
|
||||
expect(panel.destroy).toHaveBeenCalled()
|
||||
|
||||
return
|
||||
|
||||
describe "when an installed package is clicked from the Install panel", ->
|
||||
it "displays the package details", ->
|
||||
waitsFor ->
|
||||
atom.packages.activatePackage('settings-view')
|
||||
|
||||
runs ->
|
||||
settingsView.packageManager.getClient()
|
||||
spyOn(settingsView.packageManager.client, 'featuredPackages').andCallFake (callback) ->
|
||||
callback(null, [{name: 'settings-view'}])
|
||||
settingsView.showPanel('Install')
|
||||
|
||||
waitsFor ->
|
||||
settingsView.element.querySelectorAll('.package-card:not(.hidden)').length > 0
|
||||
|
||||
runs ->
|
||||
settingsView.element.querySelectorAll('.package-card:not(.hidden)')[0].click()
|
||||
|
||||
packageDetail = settingsView.element.querySelector('.package-detail .active')
|
||||
expect(packageDetail.textContent).toBe 'Settings View'
|
||||
|
||||
describe "when the active theme has settings", ->
|
||||
panel = null
|
||||
|
||||
beforeEach ->
|
||||
atom.packages.packageDirPaths.push(path.join(__dirname, 'fixtures'))
|
||||
atom.packages.loadPackage('ui-theme-with-config')
|
||||
atom.packages.loadPackage('syntax-theme-with-config')
|
||||
atom.config.set('core.themes', ['ui-theme-with-config', 'syntax-theme-with-config'])
|
||||
|
||||
reloadedHandler = jasmine.createSpy('reloadedHandler')
|
||||
atom.themes.onDidChangeActiveThemes(reloadedHandler)
|
||||
atom.themes.activatePackages()
|
||||
|
||||
waitsFor "themes to be reloaded", ->
|
||||
reloadedHandler.callCount is 1
|
||||
|
||||
runs ->
|
||||
settingsView.showPanel('Themes')
|
||||
panel = settingsView.element.querySelector('.themes-panel')
|
||||
|
||||
afterEach ->
|
||||
atom.themes.unwatchUserStylesheet()
|
||||
|
||||
describe "when the UI theme's settings button is clicked", ->
|
||||
it "navigates to that theme's detail view", ->
|
||||
jasmine.attachToDOM(settingsView.element)
|
||||
expect(panel.querySelector('.active-theme-settings')).toBeVisible()
|
||||
|
||||
panel.querySelector('.active-theme-settings').click()
|
||||
packageDetail = settingsView.element.querySelector('.package-detail li.active')
|
||||
expect(packageDetail.textContent).toBe 'Ui Theme With Config'
|
||||
|
||||
describe "when the syntax theme's settings button is clicked", ->
|
||||
it "navigates to that theme's detail view", ->
|
||||
jasmine.attachToDOM(settingsView.element)
|
||||
expect(panel.querySelector('.active-syntax-settings')).toBeVisible()
|
||||
|
||||
panel.querySelector('.active-syntax-settings').click()
|
||||
packageDetail = settingsView.element.querySelector('.package-detail li.active')
|
||||
expect(packageDetail.textContent).toBe 'Syntax Theme With Config'
|
208
packages/settings-view/spec/themes-panel-spec.coffee
Normal file
208
packages/settings-view/spec/themes-panel-spec.coffee
Normal file
@ -0,0 +1,208 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
|
||||
CSON = require 'season'
|
||||
|
||||
PackageManager = require '../lib/package-manager'
|
||||
ThemesPanel = require '../lib/themes-panel'
|
||||
|
||||
describe "ThemesPanel", ->
|
||||
[panel, packageManager, reloadedHandler] = []
|
||||
settingsView = null
|
||||
|
||||
beforeEach ->
|
||||
atom.packages.loadPackage('atom-light-ui')
|
||||
atom.packages.loadPackage('atom-dark-ui')
|
||||
atom.packages.loadPackage('atom-light-syntax')
|
||||
atom.packages.loadPackage('atom-dark-syntax')
|
||||
atom.packages.packageDirPaths.push(path.join(__dirname, 'fixtures'))
|
||||
atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax'])
|
||||
reloadedHandler = jasmine.createSpy('reloadedHandler')
|
||||
atom.themes.onDidChangeActiveThemes(reloadedHandler)
|
||||
atom.themes.activatePackages()
|
||||
|
||||
waitsFor "themes to be reloaded", ->
|
||||
reloadedHandler.callCount is 1
|
||||
|
||||
runs ->
|
||||
packageManager = new PackageManager
|
||||
themeMetadata = CSON.readFileSync(path.join(__dirname, 'fixtures', 'a-theme', 'package.json'))
|
||||
spyOn(packageManager, 'getFeatured').andCallFake (callback) ->
|
||||
Promise.resolve([themeMetadata])
|
||||
panel = new ThemesPanel(settingsView, packageManager)
|
||||
|
||||
# Make updates synchronous
|
||||
spyOn(panel, 'scheduleUpdateThemeConfig').andCallFake -> @updateThemeConfig()
|
||||
|
||||
afterEach ->
|
||||
atom.packages.unloadPackage('a-theme') if atom.packages.isPackageLoaded('a-theme')
|
||||
waitsForPromise ->
|
||||
Promise.resolve(atom.themes.deactivateThemes()) # Ensure works on promise and non-promise versions
|
||||
|
||||
it "selects the active syntax and UI themes", ->
|
||||
expect(panel.refs.uiMenu.value).toBe 'atom-dark-ui'
|
||||
expect(panel.refs.syntaxMenu.value).toBe 'atom-dark-syntax'
|
||||
|
||||
describe "when a UI theme is selected", ->
|
||||
it "updates the 'core.themes' config key with the selected UI theme", ->
|
||||
for child in panel.refs.uiMenu.children
|
||||
child.selected = child.value is 'atom-light-ui'
|
||||
child.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
waitsFor ->
|
||||
reloadedHandler.callCount is 2
|
||||
runs ->
|
||||
expect(atom.config.get('core.themes')).toEqual ['atom-light-ui', 'atom-dark-syntax']
|
||||
|
||||
describe "when a syntax theme is selected", ->
|
||||
it "updates the 'core.themes' config key with the selected syntax theme", ->
|
||||
for child in panel.refs.syntaxMenu.children
|
||||
child.selected = child.value is 'atom-light-syntax'
|
||||
child.dispatchEvent(new Event('change', {bubbles: true}))
|
||||
waitsFor ->
|
||||
reloadedHandler.callCount is 2
|
||||
runs ->
|
||||
expect(atom.config.get('core.themes')).toEqual ['atom-dark-ui', 'atom-light-syntax']
|
||||
|
||||
describe "when the 'core.config' key changes", ->
|
||||
it "refreshes the theme menus", ->
|
||||
reloadedHandler.reset()
|
||||
atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax'])
|
||||
|
||||
waitsFor ->
|
||||
reloadedHandler.callCount is 1
|
||||
|
||||
runs ->
|
||||
expect(panel.refs.uiMenu.value).toBe 'atom-light-ui'
|
||||
expect(panel.refs.syntaxMenu.value).toBe 'atom-light-syntax'
|
||||
|
||||
xdescribe "when the themes panel is navigated to", ->
|
||||
xit "focuses the search filter", ->
|
||||
settingsView.showPanel('Themes')
|
||||
expect(panel.refs.filterEditor.element).toHaveFocus()
|
||||
|
||||
describe "theme lists", ->
|
||||
[installed] = []
|
||||
beforeEach ->
|
||||
installed = JSON.parse fs.readFileSync(path.join(__dirname, 'fixtures', 'installed.json'))
|
||||
spyOn(packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(packageManager, 'getInstalled').andReturn Promise.resolve(installed)
|
||||
panel = new ThemesPanel(settingsView, packageManager)
|
||||
|
||||
waitsFor ->
|
||||
packageManager.getInstalled.callCount is 1 and panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
it 'shows the themes', ->
|
||||
expect(panel.refs.communityCount.textContent.trim()).toBe '1'
|
||||
expect(panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(panel.refs.coreCount.textContent.trim()).toBe '1'
|
||||
expect(panel.refs.corePackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(panel.refs.devCount.textContent.trim()).toBe '1'
|
||||
expect(panel.refs.devPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
it 'filters themes by name', ->
|
||||
panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
expect(panel.refs.communityCount.textContent.trim()).toBe '1/1'
|
||||
expect(panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
expect(panel.refs.coreCount.textContent.trim()).toBe '0/1'
|
||||
expect(panel.refs.corePackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
expect(panel.refs.devCount.textContent.trim()).toBe '0/1'
|
||||
expect(panel.refs.devPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 0
|
||||
|
||||
it 'adds newly installed themes to the list', ->
|
||||
[installCallback] = []
|
||||
spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
|
||||
installCallback = callback
|
||||
onWillThrowError: ->
|
||||
spyOn(atom.packages, 'loadPackage').andCallFake (name) ->
|
||||
installed.user.push {name, theme: 'ui'}
|
||||
|
||||
expect(panel.refs.communityCount.textContent.trim()).toBe '1'
|
||||
expect(panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 1
|
||||
|
||||
packageManager.install({name: 'another-user-theme', theme: 'ui'})
|
||||
installCallback(0, '', '')
|
||||
|
||||
advanceClock ThemesPanel.loadPackagesDelay()
|
||||
waits 1
|
||||
runs ->
|
||||
expect(panel.refs.communityCount.textContent.trim()).toBe '2'
|
||||
expect(panel.refs.communityPackages.querySelectorAll('.package-card:not(.hidden)').length).toBe 2
|
||||
|
||||
it 'collapses/expands a sub-section if its header is clicked', ->
|
||||
expect(panel.element.querySelectorAll('.sub-section-heading.has-items').length).toBe 3
|
||||
panel.element.querySelector('.sub-section.installed-packages .sub-section-heading.has-items').click()
|
||||
expect(panel.element.querySelector('.sub-section.installed-packages')).toHaveClass 'collapsed'
|
||||
|
||||
expect(panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
panel.element.querySelector('.sub-section.installed-packages .sub-section-heading.has-items').click()
|
||||
expect(panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'can collapse and expand any of the sub-sections', ->
|
||||
expect(panel.element.querySelectorAll('.sub-section-heading.has-items').length).toBe 3
|
||||
|
||||
for heading in panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
heading.click()
|
||||
expect(panel.element.querySelector('.sub-section.installed-packages')).toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.core-packages')).toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.dev-packages')).toHaveClass 'collapsed'
|
||||
|
||||
for heading in panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
heading.click()
|
||||
expect(panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'can collapse sub-sections when filtering', ->
|
||||
panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
|
||||
hasItems = panel.element.querySelectorAll('.sub-section-heading.has-items')
|
||||
expect(hasItems.length).toBe 1
|
||||
expect(hasItems[0].textContent).toMatch /^Community Themes/
|
||||
|
||||
describe 'when there are no themes', ->
|
||||
beforeEach ->
|
||||
installed =
|
||||
dev: []
|
||||
user: []
|
||||
core: []
|
||||
|
||||
spyOn(packageManager, 'loadCompatiblePackageVersion').andCallFake ->
|
||||
spyOn(packageManager, 'getInstalled').andReturn Promise.resolve(installed)
|
||||
panel = new ThemesPanel(settingsView, packageManager)
|
||||
|
||||
waitsFor ->
|
||||
packageManager.getInstalled.callCount is 1 and panel.refs.communityCount.textContent.indexOf('…') < 0
|
||||
|
||||
afterEach ->
|
||||
waitsForPromise ->
|
||||
Promise.resolve(atom.themes.deactivateThemes()) # Ensure works on promise and non-promise versions
|
||||
|
||||
it 'has a count of zero in all headings', ->
|
||||
for heading in panel.element.querySelector('.section-heading-count')
|
||||
expect(heading.textContent).toMatch /^0+$/
|
||||
expect(panel.element.querySelectorAll('.sub-section .icon-paintcan').length).toBe 4
|
||||
expect(panel.element.querySelectorAll('.sub-section .icon-paintcan.has-items').length).toBe 0
|
||||
|
||||
it 'can collapse and expand any of the sub-sections', ->
|
||||
for heading in panel.element.querySelectorAll('.sub-section-heading')
|
||||
heading.click()
|
||||
expect(panel.element.querySelector('.sub-section.installed-packages')).not.toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.core-packages')).not.toHaveClass 'collapsed'
|
||||
expect(panel.element.querySelector('.sub-section.dev-packages')).not.toHaveClass 'collapsed'
|
||||
|
||||
it 'does not allow collapsing on any section when filtering', ->
|
||||
panel.refs.filterEditor.setText('user-')
|
||||
window.advanceClock(panel.refs.filterEditor.getBuffer().stoppedChangingDelay)
|
||||
|
||||
for heading in panel.element.querySelector('.section-heading-count')
|
||||
expect(heading.textContent).toMatch /^(0\/0)+$/
|
||||
expect(panel.element.querySelectorAll('.sub-section .icon-paintcan').length).toBe 4
|
||||
expect(panel.element.querySelectorAll('.sub-section .icon-paintcan.has-items').length).toBe 0
|
225
packages/settings-view/spec/updates-panel-spec.coffee
Normal file
225
packages/settings-view/spec/updates-panel-spec.coffee
Normal file
@ -0,0 +1,225 @@
|
||||
UpdatesPanel = require '../lib/updates-panel'
|
||||
PackageManager = require '../lib/package-manager'
|
||||
SettingsView = require '../lib/settings-view'
|
||||
|
||||
describe 'UpdatesPanel', ->
|
||||
panel = null
|
||||
settingsView = null
|
||||
packageManager = null
|
||||
[resolveOutdated, rejectOutdated] = []
|
||||
|
||||
beforeEach ->
|
||||
settingsView = new SettingsView
|
||||
packageManager = new PackageManager
|
||||
# This spy is only needed for the Check for Updates specs,
|
||||
# but we have to instantiate it here because we need to pass the spy to the UpdatesPanel
|
||||
spyOn(packageManager, 'getOutdated').andReturn(new Promise((resolve, reject) -> [resolveOutdated, rejectOutdated] = [resolve, reject]))
|
||||
panel = new UpdatesPanel(settingsView, packageManager)
|
||||
jasmine.attachToDOM(panel.element)
|
||||
|
||||
it "shows updates when updates are available", ->
|
||||
pack =
|
||||
name: 'test-package'
|
||||
description: 'some description'
|
||||
latestVersion: '99.0.0'
|
||||
version: '1.0.0'
|
||||
|
||||
# skip packman stubbing
|
||||
panel.beforeShow(updates: [pack])
|
||||
expect(panel.refs.updatesContainer.children.length).toBe(1)
|
||||
|
||||
it "shows a message when updates are not available", ->
|
||||
panel.beforeShow(updates: [])
|
||||
expect(panel.refs.updatesContainer.children.length).toBe(0)
|
||||
expect(panel.refs.noUpdatesMessage.style.display).not.toBe('none')
|
||||
|
||||
describe "version pinned packages message", ->
|
||||
it 'shows a message when there are pinned version packages', ->
|
||||
spyOn(packageManager, 'getVersionPinnedPackages').andReturn(['foo', 'bar', 'baz'])
|
||||
panel.beforeShow(updates: [])
|
||||
expect(panel.refs.versionPinnedPackagesMessage.style.display).not.toBe('none')
|
||||
|
||||
it 'does not show a message when there are no version pinned packages', ->
|
||||
spyOn(packageManager, 'getVersionPinnedPackages').andReturn([])
|
||||
panel.beforeShow(updates: [])
|
||||
expect(panel.refs.versionPinnedPackagesMessage.style.display).toBe('none')
|
||||
|
||||
describe "the Update All button", ->
|
||||
packA =
|
||||
name: 'test-package-a'
|
||||
description: 'some description'
|
||||
latestVersion: '99.0.0'
|
||||
version: '1.0.0'
|
||||
packB =
|
||||
name: 'test-package-b'
|
||||
description: 'some description'
|
||||
latestVersion: '99.0.0'
|
||||
version: '1.0.0'
|
||||
packC =
|
||||
name: 'test-package-c'
|
||||
description: 'some description'
|
||||
latestVersion: '99.0.0'
|
||||
version: '1.0.0'
|
||||
|
||||
[cardA, cardB, cardC] = []
|
||||
[resolveA, resolveB, resolveC, rejectA, rejectB, rejectC] = []
|
||||
|
||||
beforeEach ->
|
||||
# skip packman stubbing
|
||||
panel.beforeShow(updates: [packA, packB, packC])
|
||||
|
||||
[cardA, cardB, cardC] = panel.packageCards
|
||||
|
||||
# fake a git url package
|
||||
cardC.pack.apmInstallSource = {type: 'git', sha: 'cf23df2207d99a74fbe169e3eba035e633b65d94'}
|
||||
cardC.pack.latestSha = 'a296114f3d0deec519a41f4c62e7fc56075b7f01'
|
||||
|
||||
spyOn(cardA, 'update').andReturn(new Promise((resolve, reject) -> [resolveA, rejectA] = [resolve, reject]))
|
||||
spyOn(cardB, 'update').andReturn(new Promise((resolve, reject) -> [resolveB, rejectB] = [resolve, reject]))
|
||||
spyOn(cardC, 'update').andReturn(new Promise((resolve, reject) -> [resolveC, rejectC] = [resolve, reject]))
|
||||
|
||||
atom.config.set("settings-view.packageUpdateConcurrency", -1)
|
||||
|
||||
it 'attempts to update all packages and prompts to restart if at least one package updates successfully', ->
|
||||
expect(atom.notifications.getNotifications().length).toBe 0
|
||||
expect(panel.refs.updateAllButton).toBeVisible()
|
||||
|
||||
panel.updateAll()
|
||||
|
||||
resolveA()
|
||||
rejectB('Error updating package')
|
||||
|
||||
waits 0
|
||||
runs ->
|
||||
expect(atom.notifications.getNotifications().length).toBe 0
|
||||
|
||||
resolveC()
|
||||
|
||||
waitsFor ->
|
||||
atom.notifications.getNotifications().length is 1
|
||||
|
||||
runs ->
|
||||
notifications = atom.notifications.getNotifications()
|
||||
expect(notifications.length).toBe 1
|
||||
notif = notifications[0]
|
||||
|
||||
expect(notif.options.detail).toBe 'test-package-a@1.0.0 -> 99.0.0\ntest-package-b@1.0.0 -> 99.0.0\ntest-package-c@cf23df22 -> a296114f'
|
||||
|
||||
expect(notif.options.buttons.length).toBe(2)
|
||||
|
||||
spyOn(atom, 'restartApplication')
|
||||
notif.options.buttons[0].onDidClick()
|
||||
expect(atom.restartApplication).toHaveBeenCalled()
|
||||
|
||||
spyOn(notif, 'dismiss')
|
||||
notif.options.buttons[1].onDidClick()
|
||||
expect(notif.dismiss).toHaveBeenCalled()
|
||||
|
||||
it 'works with queue enabled', ->
|
||||
expect(panel.refs.updateAllButton).not.toBeDisabled()
|
||||
atom.config.set("settings-view.packageUpdateConcurrency", 2)
|
||||
|
||||
panel.updateAll()
|
||||
|
||||
resolveA()
|
||||
resolveB()
|
||||
resolveC()
|
||||
|
||||
waitsFor ->
|
||||
panel.refs.updateAllButton.style.display is 'none'
|
||||
|
||||
it 'becomes hidden if all updates succeed', ->
|
||||
expect(panel.refs.updateAllButton).not.toBeDisabled()
|
||||
|
||||
panel.updateAll()
|
||||
|
||||
expect(panel.refs.updateAllButton).toBeDisabled()
|
||||
|
||||
resolveA()
|
||||
resolveB()
|
||||
resolveC()
|
||||
|
||||
waitsFor ->
|
||||
panel.refs.updateAllButton.style.display is 'none'
|
||||
|
||||
it 'remains enabled and visible if not all updates succeed', ->
|
||||
expect(panel.refs.updateAllButton).not.toBeDisabled()
|
||||
|
||||
panel.updateAll()
|
||||
|
||||
expect(panel.refs.updateAllButton).toBeDisabled()
|
||||
|
||||
resolveA()
|
||||
rejectB('Error updating package')
|
||||
resolveC()
|
||||
|
||||
waitsFor ->
|
||||
panel.refs.updateAllButton.disabled is false
|
||||
|
||||
runs ->
|
||||
expect(panel.refs.updateAllButton).toBeVisible()
|
||||
|
||||
it 'does not attempt to update packages that are already updating', ->
|
||||
cardA.update()
|
||||
packageManager.emitPackageEvent 'updating', packA
|
||||
panel.updateAll()
|
||||
|
||||
expect(cardA.update.calls.length).toBe 1
|
||||
|
||||
describe 'the Check for Updates button', ->
|
||||
pack =
|
||||
name: 'test-package'
|
||||
description: 'some description'
|
||||
latestVersion: '99.0.0'
|
||||
version: '1.0.0'
|
||||
|
||||
beforeEach ->
|
||||
# skip packman stubbing - without this, getOutdated() is called another time
|
||||
# this is not an issue in actual usage as getOutdated() isn't blocked on a spy
|
||||
panel.beforeShow(updates: [pack])
|
||||
|
||||
it 'disables itself when clicked until the list of outdated packages is returned', ->
|
||||
# Updates panel checks for updates on initialization so resolve the promise
|
||||
resolveOutdated()
|
||||
|
||||
waits 0
|
||||
runs ->
|
||||
expect(panel.refs.checkButton.disabled).toBe false
|
||||
|
||||
panel.checkForUpdates()
|
||||
expect(panel.refs.checkButton.disabled).toBe true
|
||||
|
||||
resolveOutdated()
|
||||
|
||||
waits 0
|
||||
runs ->
|
||||
expect(panel.refs.checkButton.disabled).toBe false
|
||||
|
||||
it 'clears the outdated cache when checking for updates', ->
|
||||
# This spec just tests that we're passing the clearCache bool through, not the actual implementation
|
||||
# For that, look at the PackageManager specs
|
||||
resolveOutdated()
|
||||
waits 0
|
||||
runs ->
|
||||
panel.refs.checkButton.click()
|
||||
expect(packageManager.getOutdated).toHaveBeenCalledWith true
|
||||
|
||||
it 'is disabled when packages are updating', ->
|
||||
# Updates panel checks for updates on initialization so resolve the promise
|
||||
resolveOutdated()
|
||||
|
||||
waits 0
|
||||
runs ->
|
||||
expect(panel.refs.checkButton.disabled).toBe false
|
||||
|
||||
packageManager.emitPackageEvent 'updating', {name: 'packA'}
|
||||
expect(panel.refs.checkButton.disabled).toBe true
|
||||
|
||||
packageManager.emitPackageEvent 'updating', {name: 'packB'}
|
||||
expect(panel.refs.checkButton.disabled).toBe true
|
||||
|
||||
packageManager.emitPackageEvent 'updated', {name: 'packB'}
|
||||
expect(panel.refs.checkButton.disabled).toBe true
|
||||
|
||||
packageManager.emitPackageEvent 'update-failed', {name: 'packA'}
|
||||
expect(panel.refs.checkButton.disabled).toBe false
|
11
packages/settings-view/spec/utils-spec.coffee
Normal file
11
packages/settings-view/spec/utils-spec.coffee
Normal file
@ -0,0 +1,11 @@
|
||||
{ownerFromRepository} = require '../lib/utils'
|
||||
|
||||
describe "Utils", ->
|
||||
describe "ownerFromRepository", ->
|
||||
it "handles a long github url", ->
|
||||
owner = ownerFromRepository("http://github.com/omgwow/some-package")
|
||||
expect(owner).toBe("omgwow")
|
||||
|
||||
it "handles a short github url", ->
|
||||
owner = ownerFromRepository("omgwow/some-package")
|
||||
expect(owner).toBe("omgwow")
|
154
packages/settings-view/styles/docks.less
Normal file
154
packages/settings-view/styles/docks.less
Normal file
@ -0,0 +1,154 @@
|
||||
@import "ui-variables";
|
||||
|
||||
// Dock overrides
|
||||
// Allows settings to be used in left/right dock
|
||||
|
||||
atom-dock.left .settings-view,
|
||||
atom-dock.right .settings-view {
|
||||
flex-direction: column;
|
||||
|
||||
&.pane-item {
|
||||
background-color: @tool-panel-background-color;
|
||||
}
|
||||
|
||||
.config-menu {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: @component-padding/2;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid @base-border-color;
|
||||
background-color: inherit;
|
||||
overflow-x: initial;
|
||||
|
||||
.nav {
|
||||
display: contents; // hide from DOM
|
||||
|
||||
& > li > a {
|
||||
padding: 0 .5em;
|
||||
border-radius: @component-border-radius;
|
||||
}
|
||||
|
||||
.icon:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.button-area {
|
||||
display: contents; // hide from DOM
|
||||
|
||||
.icon-link-external {
|
||||
font-size: 0;
|
||||
width: auto;
|
||||
margin: 0 @component-padding/2;
|
||||
padding: @component-padding/4 @component-padding/2;
|
||||
overflow: hidden;
|
||||
&:before {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panels .panels-item {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.section,
|
||||
.section:first-child, .settings-view .section:last-child {
|
||||
padding: @component-padding*1.5;
|
||||
}
|
||||
|
||||
.sub-section:not(.collapsed) .package-container {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
section .section-heading,
|
||||
.section .section-heading {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.sub-section .sub-section-heading {
|
||||
font-size: 1.15em;
|
||||
|
||||
&.has-items::before {
|
||||
margin-right: 0;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
padding: @component-padding;
|
||||
font-size: inherit;
|
||||
background-color: mix(@tool-panel-background-color, @base-background-color, 25%);
|
||||
|
||||
&:hover {
|
||||
background-color: mix(@tool-panel-background-color, @base-background-color, 75%);
|
||||
}
|
||||
|
||||
.package-name {
|
||||
font-size: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.package-version {
|
||||
font-size: .9em;
|
||||
color: @text-color-subtle;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta-user .avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.meta-user .author {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.btn-toolbar {
|
||||
.btn-group {
|
||||
display: flex;
|
||||
float: none;
|
||||
.btn {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
&.status-indicator {
|
||||
flex: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.themes-panel {
|
||||
.themes-picker-item {
|
||||
margin-top: @component-padding*1.5;
|
||||
}
|
||||
.theme-description {
|
||||
margin: @component-padding/2 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
279
packages/settings-view/styles/package-card.less
Normal file
279
packages/settings-view/styles/package-card.less
Normal file
@ -0,0 +1,279 @@
|
||||
@import "octicon-mixins";
|
||||
@import "ui-variables";
|
||||
@import "variables";
|
||||
|
||||
.settings-view {
|
||||
|
||||
// TODO: Somehow unify the "card".
|
||||
|
||||
.package-card {
|
||||
padding: @component-padding*1.5;
|
||||
margin-bottom: @component-padding;
|
||||
list-style-type: none;
|
||||
font-size: 1.2em;
|
||||
border-radius: @component-border-radius*2;
|
||||
border: 1px solid @base-border-color;
|
||||
background-color: @package-card-background-color;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: contrast(@package-card-background-color, darken(@package-card-background-color, 2%), lighten(@package-card-background-color, 2%));
|
||||
}
|
||||
&:active {
|
||||
background-color: @package-card-background-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: @package-card-disabled-background-color;
|
||||
.body,
|
||||
.avatar,
|
||||
.author,
|
||||
.stats {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
&.col-lg-4 {
|
||||
min-width: 190px;
|
||||
padding: @component-padding 0;
|
||||
padding-left: @component-padding;
|
||||
padding-right: @component-padding;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
min-height: 140px;
|
||||
color: @text-color;
|
||||
border-radius: @component-border-radius*2;
|
||||
border: 1px solid @base-border-color;
|
||||
background-color: @package-card-background-color;
|
||||
|
||||
.caption {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.package-name {
|
||||
margin-right: .5em;
|
||||
font-weight: bolder;
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
|
||||
.package-version {
|
||||
font-size: .8em;
|
||||
margin-right: @component-padding;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: @text-color;
|
||||
overflow: hidden;
|
||||
min-height: 38px;
|
||||
max-height: 38px;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* number of lines to show */
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.package-message p:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.downloads {
|
||||
color: @text-color-highlight;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1; /* number of lines to show */
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.package-status {
|
||||
right: 0px;
|
||||
top: 5px;
|
||||
text-align: center;
|
||||
|
||||
&.icon-cloud-download {
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
&.icon-check {
|
||||
color: #6CC644;
|
||||
}
|
||||
|
||||
&.icon-alert {
|
||||
color: #BD2C00;
|
||||
}
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-weight: 300;
|
||||
margin: 0 0 .2em 0;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.4;
|
||||
|
||||
.css-truncate-target {
|
||||
color: $greenDark;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
|
||||
.stats-item {
|
||||
margin-left: @component-padding*1.5;
|
||||
height: 26px;
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
vertical-align: top;
|
||||
|
||||
.icon {
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.star-box {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
.star-button {
|
||||
padding: 0 6px;
|
||||
|
||||
.octicon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
.css-truncate-target {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.package-description {
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
a.linked-octicon {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
.octicon {
|
||||
color: $greenDark;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $greenDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
border-radius: 3px;
|
||||
padding: 3px 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-user {
|
||||
flex: 1 0 auto;
|
||||
margin-top: @component-padding;
|
||||
margin-right: @component-padding;
|
||||
|
||||
.avatar {
|
||||
border-radius: @component-border-radius;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-color: hsla(0,0%,0%,.1);
|
||||
}
|
||||
|
||||
.author {
|
||||
color: @text-color-subtle;
|
||||
margin-left: @component-padding;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-controls {
|
||||
margin-top: @component-padding/2;
|
||||
|
||||
.install-button.is-installing,
|
||||
.uninstall-button.is-uninstalling {
|
||||
opacity: 1; // override disabled style
|
||||
background-image: linear-gradient(to right, transparent 50%, hsla(0,0%,0%,.15) 50%);
|
||||
background-size: 10px 100%;
|
||||
-webkit-animation: available-package-is-installing .5s linear infinite;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 0;
|
||||
width: @component-padding/2;
|
||||
min-width: 4px;
|
||||
pointer-events: none;
|
||||
transition: background .4s;
|
||||
background: @background-color-success;
|
||||
&.is-disabled {
|
||||
background: @background-color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-toolbar > .btn-group {
|
||||
float: left;
|
||||
margin: @component-padding/2 0 0 @component-padding/2;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-lower {
|
||||
padding: 13px;
|
||||
background-color: @input-background-color;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.blank-slate {
|
||||
text-align: center;
|
||||
color: $beigeDark;
|
||||
height: 132px;
|
||||
line-height: 132px;
|
||||
}
|
||||
|
||||
// End copy-paste from atom.io
|
||||
}
|
||||
|
||||
// Remove hover styles if it's in a detail view
|
||||
.package-detail .package-card {
|
||||
cursor: default;
|
||||
&:hover {
|
||||
background-color: @package-card-background-color;
|
||||
}
|
||||
&.disabled {
|
||||
background-color: @package-card-disabled-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes available-package-is-installing {
|
||||
100% { background-position: -10px 0px; }
|
||||
}
|
79
packages/settings-view/styles/package-readme.less
Normal file
79
packages/settings-view/styles/package-readme.less
Normal file
@ -0,0 +1,79 @@
|
||||
// Package README
|
||||
|
||||
@import "ui-variables";
|
||||
|
||||
.settings-view {
|
||||
|
||||
.package-readme {
|
||||
font-size: 1.25em;
|
||||
|
||||
h1 { font-size: 2em; margin-top: 1.5em; }
|
||||
h2 { font-size: 1.75em; margin-top: 1.5em; }
|
||||
h3 { font-size: 1.5em; margin-top: 1.75em; }
|
||||
h4 { font-size: 1.25em; margin-top: 1.25em; }
|
||||
h1, h2, h3, h4 {
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @text-color-info;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: @text-color-subtle;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: @text-color-subtle;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
word-break: normal;
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid @base-border-color;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-top: 1px solid @base-border-color;
|
||||
background-color: @base-background-color;
|
||||
&:nth-child(2n) {
|
||||
background-color: @background-color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: .3em .4em;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
color: @text-color-highlight;
|
||||
vertical-align: middle;
|
||||
background-color: lighten(@base-background-color, 10%);
|
||||
border: solid 1px @base-border-color;
|
||||
border-bottom-color: darken(@base-border-color, 10%);
|
||||
border-radius: @component-border-radius;
|
||||
box-shadow: inset 0 -1px 0 darken(@base-border-color, 10%);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
764
packages/settings-view/styles/settings-view.less
Normal file
764
packages/settings-view/styles/settings-view.less
Normal file
@ -0,0 +1,764 @@
|
||||
@import "octicon-mixins";
|
||||
@import "octicon-utf-codes";
|
||||
@import "ui-variables";
|
||||
|
||||
@section-padding: 2 * @component-padding;
|
||||
@breadcrumb-padding: 2 * @component-padding;
|
||||
|
||||
.settings-view {
|
||||
display: flex;
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 0;
|
||||
padding: 0 @breadcrumb-padding;
|
||||
font-size: 1.125em;
|
||||
line-height: 3rem;
|
||||
color: @text-color-subtle;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid @base-border-color;
|
||||
background-color: @base-background-color;
|
||||
|
||||
+ .section {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
> li {
|
||||
display: inline-block;
|
||||
a {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
+ li:before {
|
||||
content: "/"; // Unicode space added since inline-block means non-collapsing white-space
|
||||
padding: 0 .75em ;
|
||||
}
|
||||
}
|
||||
|
||||
> .active a {
|
||||
font-weight: 600;
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.text {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.alert-type(@tx; @bg) {
|
||||
@alert-background-color: mix(@bg, @base-background-color, 10%);
|
||||
color: contrast(@alert-background-color, darken(@tx, 20%), lighten(@tx, 20%) );
|
||||
border-color: darken(@alert-background-color, 10%);
|
||||
background-color: @alert-background-color;
|
||||
|
||||
.alert-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-info { .alert-type(@text-color-info; @background-color-info); }
|
||||
.alert-success { .alert-type(@text-color-success; @background-color-success); }
|
||||
.alert-warning { .alert-type(@text-color-warning; @background-color-warning); }
|
||||
.alert-error,
|
||||
.alert-danger { .alert-type(@text-color-error; @background-color-error); }
|
||||
|
||||
|
||||
.error-message {
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
top: -6px; // magic number... for now
|
||||
}
|
||||
|
||||
.error-details {
|
||||
.alert-type(@text-color-error; @background-color-error);
|
||||
}
|
||||
|
||||
.error-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
margin-left: @component-padding/2;
|
||||
}
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
color: @text-color;
|
||||
border-color: @button-border-color;
|
||||
background-color: @button-background-color;
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
background-color: @button-background-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.select-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.themes-picker-item .btn {
|
||||
margin-left: 2px;
|
||||
&::before {
|
||||
margin-right: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
padding-left: 2.25em;
|
||||
label {
|
||||
padding-left: 0;
|
||||
display: block;
|
||||
}
|
||||
.setting-title {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin: 0 .75em 0 -2.25em;
|
||||
}
|
||||
|
||||
.color {
|
||||
padding-left: 5em;
|
||||
.setting-title {
|
||||
margin-top: .15em;
|
||||
}
|
||||
.setting-description {
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
}
|
||||
input[type="color"] {
|
||||
float: left;
|
||||
margin: 0 0 0 -5em;
|
||||
padding: 0;
|
||||
width: 4em;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @base-border-color;
|
||||
background: darken(@base-background-color, 2%);
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
&::-webkit-color-swatch {
|
||||
border-radius: 2px;
|
||||
border: 1px solid hsla(0,0%,0%,.1);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: @component-padding*2 0;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.settings-filter {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: @section-padding;
|
||||
border-top: 1px solid @base-border-color;
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
padding: @section-padding;
|
||||
}
|
||||
&.section:empty {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
atom-text-editor[mini] {
|
||||
opacity: .75;
|
||||
.selection .region {
|
||||
background-color: contrast(@input-background-color, lighten(@input-background-color, 8%), darken(@input-background-color, 8%) );
|
||||
}
|
||||
}
|
||||
|
||||
atom-text-editor[mini].is-focused {
|
||||
opacity: 1;
|
||||
.selection .region {
|
||||
background-color: contrast(@input-background-color, lighten(@input-background-color, 12%), darken(@input-background-color, 12%) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div > .package-detail section.section.settings-panel {
|
||||
border-top: 1px solid @base-border-color;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
|
||||
&.updates-container {
|
||||
.package-container {
|
||||
> .row {
|
||||
margin-left: -@component-padding;
|
||||
margin-right: -@component-padding;
|
||||
}
|
||||
}
|
||||
.native-key-bindings, .alert-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section .section-heading,
|
||||
.section .section-heading {
|
||||
margin-bottom: @component-padding*2;
|
||||
color: @text-color-highlight;
|
||||
font-size: 1.75em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sub-section-heading.icon:before,
|
||||
.section-heading.icon:before {
|
||||
margin-right: @component-padding*.8;
|
||||
}
|
||||
|
||||
.section-heading-count {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sub-section {
|
||||
|
||||
.sub-section-heading {
|
||||
position: sticky;
|
||||
top: -1px; // -1px fixes a rounding issue on retina screens
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: @component-padding 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: @text-color-highlight;
|
||||
background-color: @base-background-color;
|
||||
-webkit-user-select: none;
|
||||
|
||||
&.has-items {
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
.icon(20px);
|
||||
content: @chevron-down;
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-section-body {
|
||||
margin-top: @component-padding;
|
||||
margin-bottom: @component-padding*3;
|
||||
margin-left: 6px;
|
||||
padding-left: 14px;
|
||||
border-left: 1px solid @base-border-color;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
.sub-section-heading.has-items:before {
|
||||
content: @chevron-right;
|
||||
}
|
||||
.package-container .package-card,
|
||||
.sub-section-body {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-label {
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 1.2em;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
color: @text-color-subtle;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: @text-color-info;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: lighten(@text-color-info, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
.config-menu {
|
||||
flex: 1.6;
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding-top: @component-padding;
|
||||
background-color: lighten(@base-background-color, 2%);
|
||||
border-right: 1px solid @base-border-color;
|
||||
overflow-x: auto;
|
||||
|
||||
.icon:before {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panels-packages{
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
|
||||
li + li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li > a {
|
||||
border-radius: 1px !important;
|
||||
padding: 12px 14px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
opacity: .85;
|
||||
transition: opacity .1s ease-in;
|
||||
box-shadow: inset 0 -1px 0 rgba(0,0,0, 7%);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 -1px 0 rgba(0,0,0, 7%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
li:first-child {margin-top:2px;}
|
||||
li:last-child {margin-bottom: 10px;}
|
||||
|
||||
li.active a, li.active a:hover {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 1px darken(#3c76dd, 10%) !important;
|
||||
|
||||
.package-author, .package-version {
|
||||
color: #fff;
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
|
||||
.package-version {
|
||||
line-height: 16px;
|
||||
margin-left: 5px;
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.package-author {
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.button-area {
|
||||
margin: 15px; // same as nav
|
||||
> .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nav > li {
|
||||
margin-top: 0;
|
||||
> a {
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-highlight;
|
||||
background-color: @background-color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav > li.active > a {
|
||||
color: @text-color-selected;
|
||||
background-color: @background-color-selected;
|
||||
}
|
||||
}
|
||||
|
||||
.panels {
|
||||
flex: 8.4;
|
||||
flex-flow: column;
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
.panels-item {
|
||||
flex: 1;
|
||||
min-width: 372px; // magic number: fit the Settings, Uninstall and Disable button
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
label {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.control-group + .control-group {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.control-group .editor-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grammar-filetypes {
|
||||
margin-top: @component-padding;
|
||||
}
|
||||
|
||||
.input-radio-group .input-label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.package-detail {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.package-keymap-table,
|
||||
.package-grammars-table,
|
||||
.package-snippets-table {
|
||||
padding-top: @component-padding;
|
||||
padding-left: @component-padding;
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.snippet-prefix {
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.package-snippets-table {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.update-message {
|
||||
padding-right: @component-padding;
|
||||
}
|
||||
|
||||
.link {
|
||||
width: -webkit-fit-content; // prevents hover area being full-width
|
||||
}
|
||||
|
||||
.link,
|
||||
.text {
|
||||
margin: 0 15px @component-padding 15px;
|
||||
}
|
||||
|
||||
.update-link,
|
||||
.repo-link {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.themes-panel {
|
||||
.control-group {
|
||||
margin-top: @component-padding*3;
|
||||
}
|
||||
.themes-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -@component-padding;
|
||||
}
|
||||
.themes-picker-item {
|
||||
flex: 1;
|
||||
min-width: 320px; // Enough for the UI Theme description
|
||||
padding: 0 @component-padding;
|
||||
.controls {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.packages {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.themes-label {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theme-chooser {
|
||||
padding-top: @component-padding*2;
|
||||
}
|
||||
|
||||
.theme-description {
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.package-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -@component-padding/2;
|
||||
padding-top: @component-padding*1.5;
|
||||
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
min-width: 130px;
|
||||
margin: @component-padding/2;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin: @component-padding/2;
|
||||
padding-left: 1px; // Counter balance since btns have margin-left: -1px
|
||||
}
|
||||
}
|
||||
|
||||
.search-message {
|
||||
margin: @component-padding 0 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: @component-padding 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
margin-left: @component-padding;
|
||||
}
|
||||
|
||||
.readme {
|
||||
margin-top: @component-padding;
|
||||
}
|
||||
|
||||
.panel-heading .label {
|
||||
margin-left: @component-padding;
|
||||
}
|
||||
|
||||
.loading-area {
|
||||
span {
|
||||
&::before {
|
||||
font-size: 1.1em;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-view {
|
||||
margin: @component-padding 0 @component-padding 0;
|
||||
}
|
||||
|
||||
.packages-panel {
|
||||
.nav {
|
||||
.badge {
|
||||
margin-left: @component-icon-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
padding-right: 2px;
|
||||
|
||||
&:hover {
|
||||
color: #2a6496;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: initial;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: @component-padding - 2px @component-padding;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
height: @component-line-height;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding-left: 1%;
|
||||
padding-right: 1%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-color: @base-border-color;
|
||||
}
|
||||
|
||||
col.keystroke {
|
||||
width: (15% - 2%);
|
||||
}
|
||||
|
||||
col.command {
|
||||
width: (40% - 2%);
|
||||
}
|
||||
|
||||
col.source {
|
||||
width: (10% - 2%);
|
||||
}
|
||||
|
||||
col.selector {
|
||||
width: (35% - 2%);
|
||||
}
|
||||
}
|
||||
|
||||
.compile-tools-heading {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.compile-tools-message {
|
||||
padding-top: @component-padding;
|
||||
|
||||
code {
|
||||
text-shadow: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.uri-history {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0 @component-padding @component-padding/2 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.thumbnail .btn-toolbar {
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A tooltip that shows the body of a snippet should treat whitespace literally
|
||||
// and should align to the left so that indentation is clearer.
|
||||
.snippet-body-tooltip .tooltip-inner {
|
||||
font-family: monospace;
|
||||
white-space: pre !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.package-updates-status-view:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.available-updates-status {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
|
||||
// Overrides
|
||||
// TODO: Refactor + cleanup
|
||||
|
||||
.settings-view {
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
> .row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.updates-heading-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.update-all-button:last-child {
|
||||
margin-left: @component-padding/2;
|
||||
}
|
||||
|
||||
.keybinding-panel .is-user {
|
||||
color: @text-color-selected;
|
||||
background-color: @background-color-selected;
|
||||
}
|
||||
|
||||
.btn-wrap-group .btn {
|
||||
margin: 0 @component-padding/2 @component-padding/2 0;
|
||||
}
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// Reset Bootstrap media queries,
|
||||
// since it breaks when resizing the treeview or in multiple panes
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
width: auto;
|
||||
}
|
||||
.col-lg-8 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
3
packages/settings-view/styles/variables.less
Normal file
3
packages/settings-view/styles/variables.less
Normal file
@ -0,0 +1,3 @@
|
||||
// Import only after ui-variables.less
|
||||
@package-card-background-color: lighten(@tool-panel-background-color, 8%);
|
||||
@package-card-disabled-background-color: darken(@package-card-background-color, 3%);
|
Loading…
Reference in New Issue
Block a user