mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
revive articledetail
This commit is contained in:
53
browser/main/Components/MarkdownPreview.js
Normal file
53
browser/main/Components/MarkdownPreview.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import shell from 'shell'
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from '../HomeContainer/lib/markdown'
|
||||
|
||||
function handleAnchorClick (e) {
|
||||
shell.openExternal(e.target.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
componentDidMount () {
|
||||
this.addListener()
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.addListener()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeListener()
|
||||
}
|
||||
|
||||
componentWillUpdate () {
|
||||
this.removeListener()
|
||||
}
|
||||
|
||||
addListener () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].addEventListener('click', handleAnchorClick)
|
||||
}
|
||||
}
|
||||
|
||||
removeListener () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].removeEventListener('click', handleAnchorClick)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + markdown(this.props.content)}}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownPreview.propTypes = {
|
||||
className: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var Markdown = require('../Mixins/Markdown')
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Markdown, ExternalLink],
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
content: React.PropTypes.string
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.addListener()
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
this.addListener()
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
this.removeListener()
|
||||
},
|
||||
componentWillUpdate: function () {
|
||||
this.removeListener()
|
||||
},
|
||||
addListener: function () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].addEventListener('click', this.openExternal)
|
||||
}
|
||||
},
|
||||
removeListener: function () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].removeEventListener('click', this.openExternal)
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + this.markdown(this.props.content)}}/>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,11 +1,7 @@
|
||||
var React = require('react')
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
mode: React.PropTypes.string
|
||||
},
|
||||
getClassName: function () {
|
||||
export default class ModeIcon extends React.Component {
|
||||
getClassName () {
|
||||
var mode = this.props.mode
|
||||
switch (mode) {
|
||||
// Script
|
||||
@@ -69,11 +65,17 @@ module.exports = React.createClass({
|
||||
return 'fa fa-fw fa-file-text-o'
|
||||
}
|
||||
return 'fa fa-fw fa-code'
|
||||
},
|
||||
render: function () {
|
||||
}
|
||||
|
||||
render () {
|
||||
var className = this.getClassName()
|
||||
return (
|
||||
<i className={this.props.className + ' ' + className}/>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ModeIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}
|
||||
@@ -1,9 +1,59 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import moment from 'moment'
|
||||
import { findWhere } from 'lodash'
|
||||
import ModeIcon from '../../Components/ModeIcon'
|
||||
import MarkdownPreview from '../../Components/MarkdownPreview'
|
||||
import CodeEditor from '../../Components/CodeEditor'
|
||||
|
||||
export default class ArticleDetail extends React.Component {
|
||||
render () {
|
||||
let { article, status, user } = this.props
|
||||
|
||||
let tags = article.Tags.length > 0 ? article.Tags.map(tag => {
|
||||
return (
|
||||
<a key={tag.id}>{tag.name}</a>
|
||||
)
|
||||
}) : (
|
||||
<span className='noTags'>Not tagged yet</span>
|
||||
)
|
||||
let folder = findWhere(user.Folders, {id: article.FolderId})
|
||||
let folderName = folder != null ? folder.name : '(unknown)'
|
||||
|
||||
return (
|
||||
<div className='ArticleDetail'></div>
|
||||
<div className='ArticleDetail show'>
|
||||
<div className='detailInfo'>
|
||||
<div className='left'>
|
||||
<div className='info'>
|
||||
<i className='fa fa-fw fa-square'/> {folderName}
|
||||
by {article.User.profileName}
|
||||
Created {moment(article.createdAt).format('YYYY/MM/DD')}
|
||||
Updated {moment(article.updatedAt).format('YYYY/MM/DD')}
|
||||
</div>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
|
||||
<div className='right'>
|
||||
<button><i className='fa fa-fw fa-edit'/></button>
|
||||
<button><i className='fa fa-fw fa-trash'/></button>
|
||||
<button><i className='fa fa-fw fa-share-alt'/></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='detailBody'>
|
||||
<div className='detailPanel'>
|
||||
<div className='header'>
|
||||
<ModeIcon className='mode' mode={article.mode}/>
|
||||
<div className='title'>{article.title}</div>
|
||||
</div>
|
||||
{article.mode === 'markdown' ? <MarkdownPreview content={article.content}/> : <CodeEditor readOnly={true} onChange={this.handleContentChange} mode={article.mode} code={article.content}/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleDetail.propTypes = {
|
||||
article: PropTypes.shape(),
|
||||
status: PropTypes.shape(),
|
||||
user: PropTypes.shape()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ProfileImage from '../../components/ProfileImage'
|
||||
import ModeIcon from '../../Components/ModeIcon'
|
||||
import moment from 'moment'
|
||||
import { IDLE_MODE, CREATE_MODE, EDIT_MODE } from '../actions'
|
||||
|
||||
export default class ArticleList extends React.Component {
|
||||
render () {
|
||||
let { articles, status } = this.props
|
||||
|
||||
let articlesEl = articles.map(article => {
|
||||
let tags = Array.isArray(article.Tags) && article.Tags.length > 0 ? article.Tags.map(tag => {
|
||||
return (
|
||||
<a key={tag.id}>#{tag.name}</a>
|
||||
)
|
||||
}) : (
|
||||
<span>Not tagged yet</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={'article-' + article.id}>
|
||||
<div className={'articleItem' + (false ? ' active' : '')}>
|
||||
<div className='top'>
|
||||
<i className='fa fa-fw fa-square'/>
|
||||
by <ProfileImage className='profileImage' size='20' email={article.User.email}/> {article.User.profileName}
|
||||
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
|
||||
</div>
|
||||
<div className='middle'>
|
||||
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== 'new' ? article.title : '(New article)'}</div>
|
||||
</div>
|
||||
<div className='bottom'>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
class ArticleList extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='ArticleList'></div>
|
||||
<div className='ArticleList'>
|
||||
{ status.mode === 'CREATE_MODE' ? (
|
||||
<div key={'article-' + article.id}>
|
||||
<div className={'articleItem'}>
|
||||
<div className='top'>
|
||||
<span className='updatedAt'>{}</span>
|
||||
</div>
|
||||
<div className='middle'>
|
||||
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>'(New article)'</div>
|
||||
</div>
|
||||
<div className='bottom'>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</div>
|
||||
) : null}
|
||||
{articlesEl}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ArticleList
|
||||
ArticleList.propTypes = {
|
||||
articles: PropTypes.array
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export default class ArticleNavigator extends React.Component {
|
||||
render () {
|
||||
let { user, status } = this.props
|
||||
if (user == null) return (<div className='ArticleNavigator'/>)
|
||||
console.log(user.Folders)
|
||||
|
||||
let activeFolder = findWhere(user.Folders, {id: status.folderId})
|
||||
|
||||
@@ -68,5 +67,8 @@ export default class ArticleNavigator extends React.Component {
|
||||
}
|
||||
|
||||
ArticleNavigator.propTypes = {
|
||||
user: PropTypes.object
|
||||
user: PropTypes.object,
|
||||
state: PropTypes.shape({
|
||||
folderId: PropTypes.number
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export const USER_UPDATE = 'USER_UPDATE'
|
||||
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
|
||||
export const SWITCH_USER = 'SWITCH_USER'
|
||||
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
|
||||
export const SWITCH_MODE = 'SWITCH_MODE'
|
||||
|
||||
export const IDLE_MODE = 'IDLE_MODE'
|
||||
export const CREATE_MODE = 'CREATE_MODE'
|
||||
export const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
export function updateUser (user) {
|
||||
return {
|
||||
@@ -9,6 +15,13 @@ export function updateUser (user) {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateArticles (userId, articles) {
|
||||
return {
|
||||
type: ARTICLE_UPDATE,
|
||||
data: {userId, articles}
|
||||
}
|
||||
}
|
||||
|
||||
export function switchUser (userId) {
|
||||
return {
|
||||
type: SWITCH_USER,
|
||||
@@ -22,3 +35,10 @@ export function switchFolder (folderId) {
|
||||
data: folderId
|
||||
}
|
||||
}
|
||||
|
||||
export function switchMode (mode) {
|
||||
return {
|
||||
type: SWITCH_MODE,
|
||||
data: mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,15 @@ class HomeContainer extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { users, user, status } = this.props
|
||||
const { users, user, status, articles, article } = this.props
|
||||
|
||||
return (
|
||||
<div className='HomeContainer'>
|
||||
<UserNavigator users={users} />
|
||||
<ArticleNavigator user={user} status={status}/>
|
||||
<ArticleTopBar/>
|
||||
<ArticleList/>
|
||||
<ArticleDetail/>
|
||||
<ArticleList articles={articles} status={status}/>
|
||||
<ArticleDetail user={user} article={article} status={status}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -49,11 +49,17 @@ function remap (state) {
|
||||
|
||||
let users = [currentUser, ...teams]
|
||||
let user = findWhere(users, {id: parseInt(status.userId, 10)})
|
||||
if (user == null) user = users[0]
|
||||
let articles = state.articles['team-' + user.id]
|
||||
let article = findWhere(users, {id: status.articleId})
|
||||
if (article == null) article = articles[0]
|
||||
|
||||
return {
|
||||
users,
|
||||
user,
|
||||
status
|
||||
status,
|
||||
articles,
|
||||
article
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +73,7 @@ HomeContainer.propTypes = {
|
||||
userId: PropTypes.string,
|
||||
folderId: PropTypes.number
|
||||
}),
|
||||
articles: PropTypes.array,
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
var request = require('superagent-promise')(require('superagent'), Promise)
|
||||
var apiUrl = require('../../../../config').apiUrl
|
||||
|
||||
export function fetchCurrentUser () {
|
||||
return request
|
||||
.get(apiUrl + 'auth/user')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchArticles (userId) {
|
||||
return request
|
||||
.get(apiUrl + 'teams/' + userId + '/articles')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
}
|
||||
|
||||
export function createTeam (input) {
|
||||
return request
|
||||
.post(apiUrl + 'teams')
|
||||
|
||||
11
browser/main/HomeContainer/lib/markdown.js
Normal file
11
browser/main/HomeContainer/lib/markdown.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import markdownit from 'markdown-it'
|
||||
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
linkify: true
|
||||
})
|
||||
|
||||
export default function markdown (content) {
|
||||
if (content == null) content = ''
|
||||
return md.render(content.toString())
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SWITCH_USER, SWITCH_FOLDER, USER_UPDATE } from './actions'
|
||||
import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, USER_UPDATE, ARTICLE_UPDATE, IDLE_MODE, CREATE_MODE, EDIT_MODE } from './actions'
|
||||
|
||||
const initialCurrentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
const initialParams = {}
|
||||
const initialStatus = {
|
||||
mode: IDLE_MODE
|
||||
}
|
||||
// init articles
|
||||
let teams = Array.isArray(initialCurrentUser.Teams) ? initialCurrentUser.Teams : []
|
||||
let users = [initialCurrentUser, ...teams]
|
||||
const initialArticles = users.reduce((res, user) => {
|
||||
res['team-' + user.id] = JSON.parse(localStorage.getItem('team-' + user.id))
|
||||
return res
|
||||
}, {})
|
||||
|
||||
function currentUser (state, action) {
|
||||
switch (action.type) {
|
||||
@@ -26,13 +35,31 @@ function status (state, action) {
|
||||
case SWITCH_FOLDER:
|
||||
state.folderId = action.data
|
||||
return state
|
||||
case SWITCH_MODE:
|
||||
state.mode = action.data
|
||||
return state
|
||||
default:
|
||||
if (state == null) return initialParams
|
||||
if (state == null) return initialStatus
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function articles (state, action) {
|
||||
switch (action.type) {
|
||||
case ARTICLE_UPDATE:
|
||||
let { userId, articles } = action.data
|
||||
let teamKey = 'team-' + userId
|
||||
localStorage.setItem(teamKey, JSON.stringify(articles))
|
||||
state[teamKey] = articles
|
||||
return state
|
||||
default:
|
||||
if (state == null) return initialArticles
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
currentUser,
|
||||
status
|
||||
status,
|
||||
articles
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import { createStore } from 'redux'
|
||||
import { Provider } from 'react-redux'
|
||||
import { updateUser } from './HomeContainer/actions'
|
||||
import { updateUser, updateArticles } from './HomeContainer/actions'
|
||||
import reducer from './HomeContainer/reducer'
|
||||
import Hq from './Services/Hq'
|
||||
import { fetchCurrentUser, fetchArticles } from './HomeContainer/lib/api'
|
||||
import { Router, Route, IndexRoute } from 'react-router'
|
||||
import MainContainer from './Containers/MainContainer'
|
||||
import LoginContainer from './Containers/LoginContainer'
|
||||
@@ -58,9 +58,22 @@ React.render((
|
||||
loadingCover.parentNode.removeChild(loadingCover)
|
||||
|
||||
// Refresh user information
|
||||
Hq.getUser()
|
||||
fetchCurrentUser()
|
||||
.then(function (res) {
|
||||
store.dispatch(updateUser(res.body))
|
||||
let user = res.body
|
||||
store.dispatch(updateUser(user))
|
||||
|
||||
let users = [user].concat(user.Teams)
|
||||
users.forEach(user => {
|
||||
fetchArticles(user.id)
|
||||
.then(res => {
|
||||
store.dispatch(updateArticles(user.id, res.body))
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status == null) throw err
|
||||
console.error(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error(err.message)
|
||||
|
||||
@@ -5,6 +5,9 @@ noTagsColor = #999
|
||||
top 60px
|
||||
left 510px
|
||||
padding 10px
|
||||
background-color #E6E6E6
|
||||
border-top 1px solid borderColor
|
||||
border-left 1px solid borderColor
|
||||
*
|
||||
-webkit-user-select all
|
||||
.detailInfo
|
||||
|
||||
@@ -6,65 +6,65 @@ articleItemColor = #777
|
||||
top 60px
|
||||
left 260px
|
||||
width 250px
|
||||
border-right solid 1px highlightenBorderColor
|
||||
&>ul
|
||||
absolute top bottom left right
|
||||
overflow-y auto
|
||||
noSelect()
|
||||
li
|
||||
.articleItem
|
||||
border solid 2px transparent
|
||||
position relative
|
||||
height 88px
|
||||
width 100%
|
||||
cursor pointer
|
||||
transition 0.1s
|
||||
background-color white
|
||||
padding 0 10px
|
||||
font-size 12px
|
||||
.top
|
||||
clearfix()
|
||||
border-top 1px solid borderColor
|
||||
border-right 4px solid #E6E6E6
|
||||
overflow-y auto
|
||||
noSelect()
|
||||
&>div
|
||||
border-right 1px solid borderColor
|
||||
.articleItem
|
||||
border solid 2px transparent
|
||||
position relative
|
||||
height 88px
|
||||
width 100%
|
||||
cursor pointer
|
||||
transition 0.1s
|
||||
background-color white
|
||||
padding 0 10px
|
||||
font-size 12px
|
||||
.top
|
||||
clearfix()
|
||||
line-height 20px
|
||||
padding 5px 0
|
||||
color articleItemColor
|
||||
.profileImage
|
||||
vertical-align middle
|
||||
.updatedAt
|
||||
float right
|
||||
line-height 20px
|
||||
padding 5px 0
|
||||
.middle
|
||||
clearfix()
|
||||
padding 3px 0 7px
|
||||
font-size 16px
|
||||
.mode
|
||||
float left
|
||||
font-size 12px
|
||||
line-height 16px
|
||||
.title
|
||||
float left
|
||||
overflow ellipsis
|
||||
padding 0 5px
|
||||
.bottom
|
||||
padding 5px 0
|
||||
overflow-x auto
|
||||
white-space nowrap
|
||||
.tags
|
||||
color articleItemColor
|
||||
.profileImage
|
||||
vertical-align middle
|
||||
.updatedAt
|
||||
float right
|
||||
line-height 20px
|
||||
.middle
|
||||
clearfix()
|
||||
padding 3px 0 7px
|
||||
font-size 16px
|
||||
.mode
|
||||
float left
|
||||
font-size 12px
|
||||
line-height 16px
|
||||
.title
|
||||
float left
|
||||
overflow ellipsis
|
||||
padding 0 5px
|
||||
.bottom
|
||||
padding 5px 0
|
||||
overflow-x auto
|
||||
white-space nowrap
|
||||
.tags
|
||||
color articleItemColor
|
||||
a
|
||||
background-color brandColor
|
||||
color white
|
||||
border-radius 2px
|
||||
padding 1.5px 5px
|
||||
margin 2px
|
||||
font-size 10px
|
||||
opacity 0.8
|
||||
&:hover
|
||||
opacity 1
|
||||
&:hover, &.hover
|
||||
background-color articleItemHoverBgColor
|
||||
&:active, &.active
|
||||
background-color white
|
||||
a
|
||||
background-color brandColor
|
||||
color white
|
||||
border-radius 2px
|
||||
padding 1.5px 5px
|
||||
margin 2px
|
||||
font-size 10px
|
||||
opacity 0.8
|
||||
&:hover
|
||||
opacity 1
|
||||
&:hover, &.hover
|
||||
background-color articleItemHoverBgColor
|
||||
&:active, &.active
|
||||
border-color brandBorderColor
|
||||
.divider
|
||||
border-bottom solid 1px borderColor
|
||||
background-color white
|
||||
&:active, &.active
|
||||
border-color brandBorderColor
|
||||
.divider
|
||||
border-bottom solid 1px borderColor
|
||||
|
||||
@@ -5,6 +5,7 @@ articleNavBgColor = #353535
|
||||
absolute top bottom
|
||||
left 60px
|
||||
width 200px
|
||||
border-right 1px solid borderColor
|
||||
color white
|
||||
.userInfo
|
||||
height 60px
|
||||
@@ -48,7 +49,7 @@ articleNavBgColor = #353535
|
||||
.header
|
||||
border-bottom 1px solid borderColor
|
||||
padding-bottom 5px
|
||||
margin-bottom 5px
|
||||
margin-bottom 10px
|
||||
clearfix()
|
||||
.title
|
||||
float left
|
||||
@@ -75,8 +76,8 @@ articleNavBgColor = #353535
|
||||
.folders
|
||||
margin-bottom 15px
|
||||
.folderList button
|
||||
height 44px
|
||||
width 200px
|
||||
height 33px
|
||||
width 199px
|
||||
border none
|
||||
text-align left
|
||||
font-size 14px
|
||||
|
||||
@@ -43,7 +43,9 @@ module.exports = {
|
||||
'react-transform-catch-errors',
|
||||
'redux-devtools',
|
||||
'redux-devtools/lib/react',
|
||||
'react-select'
|
||||
'react-select',
|
||||
'markdown-it',
|
||||
'moment'
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx', 'styl']
|
||||
|
||||
Reference in New Issue
Block a user