This repository was archived by the owner on Jul 28, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 55
Add search command (WIP) #11
Open
chrisforrette
wants to merge
21
commits into
npm:latest
Choose a base branch
from
chrisforrette:feature/search-command
base: latest
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
d390a5c
Adding basic foundation for search command
chrisforrette da1c070
Trying to JSX the search
chrisforrette aea8899
Fleshing out a bunch of the interactive search component composition …
chrisforrette 799c11d
Getting fake install flow working
chrisforrette fc84e7c
Customizing package listing details and select components
chrisforrette 7204636
Fixing inline search term search
chrisforrette a009788
deps: Adding figures@2.0.0
chrisforrette 4a0bd88
fix(search): making search command JSX-ish and removing ink 'h' funct…
chrisforrette 64c7e71
fix(search): adding ink 'h' import back to search command, ink gets m…
chrisforrette 0588526
fix(search): changing search component export to be the search compon…
chrisforrette 4dca41e
fix(search): added maintenance/popularity/quality legend to results t…
chrisforrette c45cbbd
feat(search): adding options config object with almost all libnpm sea…
chrisforrette 096c2a0
fix(search): removing special character stuff: package selector indic…
chrisforrette 6842d04
deps: removing figures and ink-spinner
chrisforrette 989b967
feat(search): worked out a decent control flow, removing focus on sel…
chrisforrette ead33b5
feat(search): making it so that the search input prompt doesn't displ…
chrisforrette c521af8
feat(search): fixing issue with focus remaining on search results whe…
chrisforrette 45fa41a
feat(search): linting
chrisforrette ff28c51
feat(search): removing stray todo note
chrisforrette 6e583e9
feat(search): updating search command, moving it into yargs-modules, …
chrisforrette 60091f5
Updating package-lock.json, removing unnecessary eslint comment
chrisforrette File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| 'use strict' | ||
|
|
||
| const { | ||
| h, // eslint-disable-line no-unused-vars | ||
| Color, | ||
| Component, | ||
| Fragment | ||
| } = require('ink') | ||
| const SelectInput = require('ink-select-input') | ||
| const TextInput = require('ink-text-input') | ||
| const libnpm = require('libnpm') | ||
|
|
||
| const FOCUS_SEARCH = 'FOCUS_SEARCH' | ||
| const FOCUS_RESULTS = 'FOCUS_RESULTS' | ||
|
|
||
| class Search extends Component { | ||
| constructor (props) { | ||
| super(props) | ||
| const { terms } = this.props | ||
| this.state = { | ||
| isInstalling: false, | ||
| isLoading: null, | ||
| focusedOn: FOCUS_SEARCH, | ||
| matches: [], | ||
| selectedPackage: null, | ||
| terms: terms || '' | ||
| } | ||
|
|
||
| this.onChangeTerms = this.onChangeTerms.bind(this) | ||
| this.onKeyPress = this.onKeyPress.bind(this) | ||
| this.onSubmit = this.onSubmit.bind(this) | ||
| this.onSelectPackage = this.onSelectPackage.bind(this) | ||
| } | ||
|
|
||
| componentDidMount () { | ||
| const { terms } = this.props | ||
| process.stdin.on('keypress', this.onKeyPress) | ||
|
|
||
| if (terms) { | ||
| this.search(terms) | ||
| } | ||
| } | ||
|
|
||
| componentWillUnmount () { | ||
| process.stdin.removeListener('keypress', this.onKeyPress) | ||
| } | ||
|
|
||
| render () { | ||
| const { | ||
| focusedOn, | ||
| isInstalling, | ||
| isLoading, | ||
| matches, | ||
| selectedPackage, | ||
| terms | ||
| } = this.state | ||
|
|
||
| return <div> | ||
| { !isInstalling && <Fragment> | ||
| <SearchInput | ||
| isFocused={focusedOn === FOCUS_SEARCH} | ||
| terms={terms} | ||
| onChange={this.onChangeTerms} | ||
| onSubmit={this.onSubmit} /> | ||
| <SearchResults | ||
| isFocused={focusedOn === FOCUS_RESULTS} | ||
| isLoading={isLoading} | ||
| terms={terms} | ||
| matches={matches} | ||
| onSelect={this.onSelectPackage} /> | ||
| </Fragment> } | ||
| { isInstalling && selectedPackage | ||
| ? <InstallingPackage isInstalling={isInstalling} pkg={selectedPackage} /> | ||
| : null | ||
| } | ||
| </div> | ||
| } | ||
|
|
||
| async search (terms) { | ||
| const { options } = this.props | ||
| try { | ||
| this.setState({ | ||
| focusedOn: FOCUS_SEARCH, | ||
| isLoading: true, | ||
| matches: [] | ||
| }) | ||
| const matches = await libnpm.search(terms, { | ||
| detailed: true, | ||
| ...options | ||
| }) | ||
| this.setState({ | ||
| focusedOn: matches && matches.length ? FOCUS_RESULTS : FOCUS_SEARCH, | ||
| isLoading: false, | ||
| matches | ||
| }) | ||
| } catch (err) { | ||
| // @todo Show error message | ||
| this.setState({ | ||
| isLoading: false, | ||
| matches: [] | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| async install (pkg) { | ||
| try { | ||
| this.setState({ | ||
| isInstalling: true, | ||
| selectedPackage: pkg | ||
| }) | ||
| // @todo Implement real install | ||
| await new Promise((resolve, reject) => setTimeout(resolve, 1000)) | ||
| this.setState({ isInstalling: false }) | ||
| this.props.onExit() | ||
| } catch (err) { | ||
| // @todo Show error message | ||
| this.setState({ isInstalling: false }) | ||
| } | ||
| } | ||
|
|
||
| onChangeTerms (terms) { | ||
| this.setState({ terms }) | ||
| } | ||
|
|
||
| onKeyPress (chunk, key) { | ||
| const { | ||
| focusedOn, | ||
| matches, | ||
| selectedPackage | ||
| } = this.state | ||
|
|
||
| // If up/down arrows are pressed, make sure we're focused on search results | ||
|
|
||
| if (['up', 'down'].includes(key.name)) { | ||
| if (focusedOn !== FOCUS_RESULTS && matches && matches.length) { | ||
| return this.setState({ focusedOn: FOCUS_RESULTS }) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // If enter is pressed and we're focused on search results, install | ||
|
|
||
| if (key.name === 'enter' && focusedOn === FOCUS_RESULTS && selectedPackage) { | ||
| return this.install(selectedPackage) | ||
| } | ||
|
|
||
| // If non up/down/enter keys were pressed and we're not focused on search | ||
| // input, focus on search input | ||
|
|
||
| if (focusedOn !== FOCUS_SEARCH) { | ||
| return this.setState({ focusedOn: FOCUS_SEARCH }) | ||
| } | ||
| } | ||
|
|
||
| onSubmit (terms) { | ||
| this.search(terms) | ||
| } | ||
|
|
||
| onSelectPackage (pkg) { | ||
| this.install(pkg) | ||
| } | ||
| } | ||
|
|
||
| const SearchInput = ({ isFocused, onChange, onSubmit, terms }) => { | ||
| return <div> | ||
| <Color grey bold>Find a package: </Color> | ||
| <TextInput focused={isFocused} value={terms} onChange={onChange} onSubmit={onSubmit} /> | ||
| </div> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For some reason, if I erase the textinput data and press "Enter" it does BOTH a search and an install (and closes the process). Or was I testing an older version?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea! This is that weird input focus thing I mentioned in my Todos list. I saw that the |
||
| } | ||
|
|
||
| const SearchResults = ({ isFocused, isLoading, matches, onSelect, terms }) => { | ||
| if (isLoading) { | ||
| return <div><Color green>Searching...</Color></div> | ||
| } | ||
|
|
||
| if (isLoading === false && terms && (!matches || !matches.length)) { | ||
| return <Color grey>No matches found</Color> | ||
| } | ||
|
|
||
| if (isLoading === false && matches && matches.length) { | ||
| return <div> | ||
| <Color grey bold>Results (maintenance, popularity, quality):</Color><br /> | ||
| <PackageSelector isFocused={isFocused} matches={matches} onSelect={onSelect} /> | ||
| </div> | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| const PackageSelectIndicator = ({ isSelected }) => { | ||
| if (!isSelected) { | ||
| return ' ' | ||
| } | ||
|
|
||
| return <Color green>{ '> ' }</Color> | ||
| } | ||
|
|
||
| const formatPackageScore = num => `${Math.round(num * 100)}%` | ||
|
|
||
| const PackageItem = ({ isSelected, value }) => { | ||
| const { | ||
| package: { | ||
| name, | ||
| publisher: { username } | ||
| }, | ||
| score: { | ||
| detail: { | ||
| maintenance, | ||
| popularity, | ||
| quality | ||
| } | ||
| } | ||
| } = value | ||
|
|
||
| const m = formatPackageScore(maintenance) | ||
| const p = formatPackageScore(popularity) | ||
| const q = formatPackageScore(quality) | ||
chrisforrette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return <Color green={isSelected}>{name} @{username} ({m} / {p} / {q})</Color> | ||
| } | ||
|
|
||
| const PackageSelector = ({ isFocused, matches, onSelect }) => { | ||
| const items = matches.map(match => ({ | ||
| value: match, | ||
| label: match | ||
| })) | ||
|
|
||
| if (isFocused) { | ||
| return <SelectInput | ||
| indicatorComponent={PackageSelectIndicator} | ||
| items={items} | ||
| itemComponent={PackageItem} | ||
| onSelect={({ value }) => onSelect(value)} /> | ||
| } else if (items && items.length) { | ||
| return items.map(({ value }) => <div>{ ' ' }<PackageItem value={value} /></div>) | ||
| } | ||
| } | ||
|
|
||
| const InstallingPackage = ({ isInstalling, pkg }) => { | ||
| if (!isInstalling) { | ||
| return null | ||
| } | ||
|
|
||
| return <div><Color green>Installing {pkg.package.name}...</Color></div> | ||
| } | ||
|
|
||
| module.exports = Search | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| 'use strict' | ||
|
|
||
| const { h, render } = require('ink') | ||
| const Search = require('../components/search.jsx') | ||
|
|
||
| const DEFAULT_LIMIT = 10 | ||
|
|
||
| const SearchCommand = module.exports = { | ||
| command: 'search', | ||
| aliases: ['s', 'se', 'find'], | ||
| describe: 'Search the NPM registry', | ||
| builder (y) { | ||
| return y.help().alias('help', 'h') | ||
| .options(SearchCommand.options) | ||
| }, | ||
| options: { | ||
| limit: { | ||
| alias: 'l', | ||
| default: DEFAULT_LIMIT, | ||
| description: 'Number of results to limit the query to', | ||
| type: 'number' | ||
| }, | ||
| maintenance: { | ||
| alias: 'm', | ||
| description: 'Decimal number between `0` and `1` that defines the weight of `maintenance` metrics when scoring and sorting packages', | ||
| type: 'number' | ||
| }, | ||
| offset: { | ||
| alias: 'o', | ||
| description: 'Offset number for results. Used with `--limit` for pagination', | ||
| type: 'number' | ||
| }, | ||
| popularity: { | ||
| alias: 'p', | ||
| description: 'Decimal number between `0` and `1` that defines the weight of `popularity` metrics when scoring and sorting packages', | ||
| type: 'number' | ||
| }, | ||
| quality: { | ||
| alias: 'q', | ||
| description: 'Decimal number between `0` and `1` that defines the weight of `quality` metrics when scoring and sorting packages', | ||
| type: 'number' | ||
| }, | ||
| sortBy: { | ||
| alias: 's', | ||
| choices: [ | ||
| 'maintenance', | ||
| 'optimal', | ||
| 'popularity', | ||
| 'quality' | ||
| ], | ||
| description: 'Used as a shorthand to set `--quality`, `--maintenance`, and `--popularity` with values that prioritize each one' | ||
| } | ||
| }, | ||
| handler: search | ||
| } | ||
|
|
||
| async function search (argv) { | ||
| let unmount | ||
|
|
||
| const onError = () => { | ||
| unmount() | ||
| process.exit(1) | ||
| } | ||
|
|
||
| const onExit = () => { | ||
| unmount() | ||
| process.exit() | ||
| } | ||
|
|
||
| const terms = argv._[1] | ||
|
|
||
| unmount = render(h(Search, { | ||
| onError, | ||
| onExit, | ||
| options: argv, | ||
| terms | ||
| })) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.