1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

cleaned up redundant variables, fixed eslint fix command, split snippetList into component

This commit is contained in:
Nguyễn Việt Hưng
2018-05-21 18:32:41 +07:00
parent ce594b0b5a
commit 2b2f17525e
13 changed files with 182 additions and 125 deletions

View File

@@ -9,7 +9,7 @@ import iconv from 'iconv-lite'
import crypto from 'crypto' import crypto from 'crypto'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import fs from 'fs' import fs from 'fs'
const { ipcRenderer, remote } = require('electron') const { ipcRenderer } = require('electron')
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -201,11 +201,11 @@ export default class CodeEditor extends React.Component {
for (let i = 0; i < snippets.length; i++) { for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
if (snippets[i].content.indexOf(templateCursorString) !== -1) { if (snippets[i].content.indexOf(templateCursorString) !== -1) {
let snippetLines = snippets[i].content.split('\n') const snippetLines = snippets[i].content.split('\n')
let cursorLineNumber = 0 let cursorLineNumber = 0
let cursorLinePosition = 0 let cursorLinePosition = 0
for (let j = 0; j < snippetLines.length; j++) { for (let j = 0; j < snippetLines.length; j++) {
let cursorIndex = snippetLines[j].indexOf(templateCursorString) const cursorIndex = snippetLines[j].indexOf(templateCursorString)
if (cursorIndex !== -1) { if (cursorIndex !== -1) {
cursorLineNumber = j cursorLineNumber = j
cursorLinePosition = cursorIndex cursorLinePosition = cursorIndex

View File

@@ -393,7 +393,7 @@ export default class MarkdownPreview extends React.Component {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
}) })
} }
let renderedHTML = this.markdown.render(value) const renderedHTML = this.markdown.render(value)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {

View File

@@ -2,7 +2,6 @@ const path = require('path')
const fs = require('sander') const fs = require('sander')
const { remote } = require('electron') const { remote } = require('electron')
const { app } = remote const { app } = remote
const os = require('os')
const themePath = process.env.NODE_ENV === 'production' const themePath = process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './node_modules/codemirror/theme') ? path.join(app.getAppPath(), './node_modules/codemirror/theme')

View File

@@ -183,7 +183,7 @@ class SideNav extends React.Component {
).filter( ).filter(
note => activeTags.every(tag => note.tags.includes(tag)) note => activeTags.every(tag => note.tags.includes(tag))
) )
let relatedTags = new Set() const relatedTags = new Set()
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag))) relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
return relatedTags return relatedTags
} }
@@ -222,7 +222,7 @@ class SideNav extends React.Component {
handleClickNarrowToTag (tag) { handleClickNarrowToTag (tag) {
const { router } = this.context const { router } = this.context
const { location } = this.props const { location } = this.props
let listOfTags = this.getActiveTags(location.pathname) const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag) const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) { if (indexOfTag > -1) {
listOfTags.splice(indexOfTag, 1) listOfTags.splice(indexOfTag, 1)

View File

@@ -104,8 +104,8 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const fileType = file['type'] const fileType = file['type']
copyAttachment(filePath, storageKey, noteKey).then((fileName) => { copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
let showPreview = fileType.startsWith('image') const showPreview = fileType.startsWith('image')
let imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview) const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
codeEditor.insertAttachmentMd(imageMd) codeEditor.insertAttachmentMd(imageMd)
}) })
} }
@@ -139,7 +139,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey) createAttachmentDestinationFolder(targetStorage.path, noteKey)
let imageName = `${uniqueSlug()}.png` const imageName = `${uniqueSlug()}.png`
const imagePath = path.join(destinationDir, imageName) const imagePath = path.join(destinationDir, imageName)
reader.onloadend = function () { reader.onloadend = function () {
@@ -147,7 +147,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
base64data += base64data.replace('+', ' ') base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary') const binaryData = new Buffer(base64data, 'base64').toString('binary')
fs.writeFile(imagePath, binaryData, 'binary') fs.writeFile(imagePath, binaryData, 'binary')
let imageMd = generateAttachmentMarkdown(imageName, imagePath, true) const imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
codeEditor.insertAttachmentMd(imageMd) codeEditor.insertAttachmentMd(imageMd)
} }
reader.readAsDataURL(blob) reader.readAsDataURL(blob)

View File

@@ -1,5 +1,4 @@
import fs from 'fs' import fs from 'fs'
import crypto from 'crypto'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
function fetchSnippet (id, snippetFile) { function fetchSnippet (id, snippetFile) {

View File

@@ -1,8 +1,6 @@
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import React from 'react' import React from 'react'
import _ from 'lodash' import _ from 'lodash'
import fs from 'fs'
import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
@@ -71,10 +69,7 @@ export default class SnippetEditor extends React.Component {
return ( return (
<div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{ <div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{
fontFamily: fontFamily.join(', '), fontFamily: fontFamily.join(', '),
fontSize: fontSize, fontSize: fontSize
position: 'absolute',
width: '100%',
height: '90%'
}} /> }} />
) )
} }

View File

@@ -0,0 +1,87 @@
import React from 'react'
import styles from './SnippetTab.styl'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import i18n from 'browser/lib/i18n'
import eventEmitter from 'browser/main/lib/eventEmitter'
const { remote } = require('electron')
const { Menu, MenuItem } = remote
class SnippetList extends React.Component {
constructor (props) {
super(props)
this.state = {
snippets: []
}
}
componentDidMount () {
this.reloadSnippetList()
eventEmitter.on('snippetList:reload', this.reloadSnippetList.bind(this))
}
reloadSnippetList () {
dataApi.fetchSnippet().then(snippets => this.setState({snippets}))
}
handleSnippetContextMenu (snippet) {
const menu = new Menu()
menu.append(new MenuItem({
label: i18n.__('Delete snippet'),
click: () => {
this.deleteSnippet(snippet)
}
}))
menu.popup()
}
deleteSnippet (snippet) {
dataApi.deleteSnippet(snippet).then(() => {
this.reloadSnippetList()
this.props.onSnippetDeleted(snippet)
}).catch(err => { throw err })
}
handleSnippetClick (snippet) {
this.props.onSnippetClick(snippet)
}
createSnippet () {
dataApi.createSnippet().then(() => {
this.reloadSnippetList()
// scroll to end of list when added new snippet
const snippetList = document.getElementById('snippets')
snippetList.scrollTop = snippetList.scrollHeight
}).catch(err => { throw err })
}
render () {
const { snippets } = this.state
return (
<div styleName='snippet-list'>
<div styleName='group-section'>
<div styleName='group-section-control'>
<button styleName='group-control-button' onClick={() => this.createSnippet()}>
<i className='fa fa-plus' /> {i18n.__('New Snippet')}
</button>
</div>
</div>
<ul id='snippets' styleName='snippets'>
{
snippets.map((snippet) => (
<li
styleName='snippet-item'
key={snippet.id}
onContextMenu={() => this.handleSnippetContextMenu(snippet)}
onClick={() => this.handleSnippetClick(snippet)}>
{snippet.name}
</li>
))
}
</ul>
</div>
)
}
}
export default CSSModules(SnippetList, styles)

View File

@@ -1,33 +1,33 @@
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl' import styles from './SnippetTab.styl'
import fs from 'fs'
import SnippetEditor from './SnippetEditor' import SnippetEditor from './SnippetEditor'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import consts from 'browser/lib/consts' import SnippetList from './SnippetList'
import eventEmitter from 'browser/main/lib/eventEmitter'
const { remote } = require('electron')
const { Menu, MenuItem } = remote
class SnippetTab extends React.Component { class SnippetTab extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
snippets: [],
currentSnippet: null currentSnippet: null
} }
this.changeDelay = null this.changeDelay = null
} }
componentDidMount () { handleSnippetNameOrPrefixChange () {
this.reloadSnippetList() clearTimeout(this.changeDelay)
this.changeDelay = setTimeout(() => {
// notify the snippet editor that the name or prefix of snippet has been changed
this.snippetEditor.onSnippetNameOrPrefixChanged(this.state.currentSnippet)
eventEmitter.emit('snippetList:reload')
}, 500)
} }
handleSnippetClick (snippet) { handleSnippetClick (snippet) {
if (this.state.currentSnippet === null || this.state.currentSnippet.id !== snippet.id) { const { currentSnippet } = this.state
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
dataApi.fetchSnippet(snippet.id).then(changedSnippet => { dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
// notify the snippet editor to load the content of the new snippet // notify the snippet editor to load the content of the new snippet
this.snippetEditor.onSnippetChanged(changedSnippet) this.snippetEditor.onSnippetChanged(changedSnippet)
@@ -36,70 +36,27 @@ class SnippetTab extends React.Component {
} }
} }
handleSnippetNameOrPrefixChange () { onSnippetNameOrPrefixChanged (e, type) {
clearTimeout(this.changeDelay) const newSnippet = Object.assign({}, this.state.currentSnippet)
this.changeDelay = setTimeout(() => { if (type === 'name') {
// notify the snippet editor that the name or prefix of snippet has been changed newSnippet.name = e.target.value
this.snippetEditor.onSnippetNameOrPrefixChanged(this.state.currentSnippet) } else {
this.reloadSnippetList() newSnippet.prefix = e.target.value
}, 500) }
this.setState({ currentSnippet: newSnippet })
this.handleSnippetNameOrPrefixChange()
} }
handleSnippetContextMenu (snippet) { handleDeleteSnippet (snippet) {
const menu = new Menu()
menu.append(new MenuItem({
label: i18n.__('Delete snippet'),
click: () => {
this.deleteSnippet(snippet)
}
}))
menu.popup()
}
reloadSnippetList () {
dataApi.fetchSnippet().then(snippets => this.setState({snippets}))
}
deleteSnippet (snippet) {
dataApi.deleteSnippet(snippet).then(() => {
this.reloadSnippetList()
// prevent old snippet still display when deleted // prevent old snippet still display when deleted
if (snippet.id === this.state.currentSnippet.id) { if (snippet.id === this.state.currentSnippet.id) {
this.setState({currentSnippet: null}) this.setState({currentSnippet: null})
} }
}).catch(err => { throw err })
}
createSnippet () {
dataApi.createSnippet().then(() => {
this.reloadSnippetList()
// scroll to end of list when added new snippet
const snippetList = document.getElementById('snippets')
snippetList.scrollTop = snippetList.scrollHeight
}).catch(err => { throw err })
}
renderSnippetList () {
const { snippets } = this.state
return (
<ul id='snippets' style={{height: 'calc(100% - 8px)', overflow: 'scroll', background: '#f5f5f5'}}>
{
snippets.map((snippet) => (
<li
styleName='snippet-item'
key={snippet.id}
onContextMenu={() => this.handleSnippetContextMenu(snippet)}
onClick={() => this.handleSnippetClick(snippet)}>
{snippet.name}
</li>
))
}
</ul>
)
} }
render () { render () {
const { config } = this.props const { config, storageKey } = this.props
const { currentSnippet } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -108,29 +65,17 @@ class SnippetTab extends React.Component {
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='header'>{i18n.__('Snippets')}</div> <div styleName='header'>{i18n.__('Snippets')}</div>
<div styleName='snippet-list'> <SnippetList
<div styleName='group-section'> onSnippetClick={this.handleSnippetClick.bind(this)}
<div styleName='group-section-control'> onSnippetDeleted={this.handleDeleteSnippet.bind(this)} />
<button styleName='group-control-button' onClick={() => this.createSnippet()}> <div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
<i className='fa fa-plus' /> {i18n.__('New Snippet')}
</button>
</div>
</div>
{this.renderSnippetList()}
</div>
<div styleName='snippet-detail' style={{visibility: this.state.currentSnippet ? 'visible' : 'hidden'}}>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div> <div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input <input
styleName='group-section-control-input' styleName='group-section-control-input'
value={this.state.currentSnippet ? this.state.currentSnippet.name : ''} value={currentSnippet ? currentSnippet.name : ''}
onChange={e => { onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'name') }}
const newSnippet = Object.assign({}, this.state.currentSnippet)
newSnippet.name = e.target.value
this.setState({ currentSnippet: newSnippet })
this.handleSnippetNameOrPrefixChange()
}}
type='text' /> type='text' />
</div> </div>
</div> </div>
@@ -139,19 +84,14 @@ class SnippetTab extends React.Component {
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input <input
styleName='group-section-control-input' styleName='group-section-control-input'
value={this.state.currentSnippet ? this.state.currentSnippet.prefix : ''} value={currentSnippet ? currentSnippet.prefix : ''}
onChange={e => { onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'prefix') }}
const newSnippet = Object.assign({}, this.state.currentSnippet)
newSnippet.prefix = e.target.value
this.setState({ currentSnippet: newSnippet })
this.handleSnippetNameOrPrefixChange()
}}
type='text' /> type='text' />
</div> </div>
</div> </div>
<div styleName='snippet-editor-section'> <div styleName='snippet-editor-section'>
<SnippetEditor <SnippetEditor
storageKey={this.props.storageKey} storageKey={storageKey}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}

View File

@@ -1,4 +1,5 @@
@import('./Tab') @import('./Tab')
@import('./ConfigTab')
.root .root
padding 15px padding 15px
@@ -96,6 +97,11 @@
height calc(100% - 200px) height calc(100% - 200px)
position absolute position absolute
.snippets
height calc(100% - 8px)
overflow scroll
background: #f5f5f5
.snippet-item .snippet-item
height 50px height 50px
font-size 15px font-size 15px
@@ -121,3 +127,20 @@
height calc(100% - 200px) height calc(100% - 200px)
position absolute position absolute
left 33% left 33%
.SnippetEditor
position absolute
width 100%
height 90%
body[data-theme="dark"]
.snippets
background: #2E3235
.snippet-item
color white
&::after
background rgba(255, 255, 255 0.1)
&:hover
background darken(#2E3235, 5)
.snippet-detail
color white

View File

@@ -12,7 +12,7 @@
"compile": "grunt compile", "compile": "grunt compile",
"test": "PWD=$(pwd) NODE_ENV=test ava --serial", "test": "PWD=$(pwd) NODE_ENV=test ava --serial",
"jest": "jest", "jest": "jest",
"fix": "npm run lint --fix", "fix": "eslint . --fix",
"lint": "eslint .", "lint": "eslint .",
"dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\"" "dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\""
}, },
@@ -118,7 +118,7 @@
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-config-standard-jsx": "^3.2.0", "eslint-config-standard-jsx": "^3.2.0",
"eslint-plugin-react": "^7.2.0", "eslint-plugin-react": "^7.8.2",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"faker": "^3.1.0", "faker": "^3.1.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",

View File

@@ -28,7 +28,6 @@ test.serial('Delete a snippet', (t) => {
.then(function assert (data) { .then(function assert (data) {
data = data[0] data = data[0]
const snippets = JSON.parse(sander.readFileSync(snippetFile)) const snippets = JSON.parse(sander.readFileSync(snippetFile))
const snippet = snippets.find(currentSnippet => currentSnippet.id === data.id)
t.is(snippets.length, 0) t.is(snippets.length, 0)
}) })
}) })

View File

@@ -2550,6 +2550,12 @@ doctrine@^2.0.0:
esutils "^2.0.2" esutils "^2.0.2"
isarray "^1.0.0" isarray "^1.0.0"
doctrine@^2.0.2:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
dependencies:
esutils "^2.0.2"
dom-serializer@0: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -2963,13 +2969,14 @@ eslint-plugin-promise@~3.4.0:
version "3.4.2" version "3.4.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.4.2.tgz#1be2793eafe2d18b5b123b8136c269f804fe7122" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.4.2.tgz#1be2793eafe2d18b5b123b8136c269f804fe7122"
eslint-plugin-react@^7.2.0: eslint-plugin-react@^7.8.2:
version "7.2.0" version "7.8.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.2.0.tgz#25c77a4ec307e3eebb248ea3350960e372ab6406" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.8.2.tgz#e95c9c47fece55d2303d1a67c9d01b930b88a51d"
dependencies: dependencies:
doctrine "^2.0.0" doctrine "^2.0.2"
has "^1.0.1" has "^1.0.1"
jsx-ast-utils "^2.0.0" jsx-ast-utils "^2.0.1"
prop-types "^15.6.0"
eslint-plugin-react@~6.7.1: eslint-plugin-react@~6.7.1:
version "6.7.1" version "6.7.1"
@@ -5330,9 +5337,9 @@ jsx-ast-utils@^1.3.3:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
jsx-ast-utils@^2.0.0: jsx-ast-utils@^2.0.1:
version "2.0.0" version "2.0.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.0.tgz#ec06a3d60cf307e5e119dac7bad81e89f096f0f8" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
dependencies: dependencies:
array-includes "^3.0.3" array-includes "^3.0.3"
@@ -6962,6 +6969,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8:
loose-envify "^1.3.1" loose-envify "^1.3.1"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types@^15.6.0:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@~15.5.7: prop-types@~15.5.7:
version "15.5.10" version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"