mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
CRUD done
This commit is contained in:
@@ -6,7 +6,7 @@ import ArticleNavigator from './HomePage/ArticleNavigator'
|
||||
import ArticleTopBar from './HomePage/ArticleTopBar'
|
||||
import ArticleList from './HomePage/ArticleList'
|
||||
import ArticleDetail from './HomePage/ArticleDetail'
|
||||
import { findWhere, pick } from 'lodash'
|
||||
import { findWhere, findIndex, pick } from 'lodash'
|
||||
import keygen from 'boost/keygen'
|
||||
import { NEW } from './actions'
|
||||
|
||||
@@ -50,9 +50,20 @@ function remap (state) {
|
||||
let activeUser = findWhere(users, {id: parseInt(status.userId, 10)})
|
||||
if (activeUser == null) activeUser = users[0]
|
||||
let articles = state.articles['team-' + activeUser.id]
|
||||
let activeArticle = findWhere(users, {id: status.articleId})
|
||||
let activeArticle = findWhere(articles, {id: status.articleId})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
// remove Unsaved new article if user is not CREATE_MODE
|
||||
if (status.mode !== CREATE_MODE) {
|
||||
let targetIndex = findIndex(articles, article => article.status === NEW)
|
||||
|
||||
if (targetIndex >= 0) articles.splice(targetIndex, 1)
|
||||
}
|
||||
|
||||
// switching CREATE_MODE
|
||||
// restrict
|
||||
// 1. team have one folder at least
|
||||
// or Change IDLE MODE
|
||||
if (status.mode === CREATE_MODE && activeUser.Folders.length > 0) {
|
||||
var newArticle = findWhere(articles, {status: 'NEW'})
|
||||
if (newArticle == null) {
|
||||
@@ -72,10 +83,6 @@ function remap (state) {
|
||||
} else if (status.mode === CREATE_MODE) {
|
||||
status.mode = IDLE_MODE
|
||||
}
|
||||
if (status.mode !== CREATE_MODE && activeArticle != null && activeArticle.status === NEW) {
|
||||
articles.splice(articles.indexOf(activeArticle), 1)
|
||||
activeArticle = articles[0]
|
||||
}
|
||||
|
||||
return {
|
||||
users,
|
||||
|
||||
@@ -4,10 +4,11 @@ import { findWhere, uniq } from 'lodash'
|
||||
import ModeIcon from 'boost/components/ModeIcon'
|
||||
import MarkdownPreview from 'boost/components/MarkdownPreview'
|
||||
import CodeEditor from 'boost/components/CodeEditor'
|
||||
import { NEW, IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode } from '../actions'
|
||||
import { UNSYNCED, IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode, switchArticle, updateArticle, destroyArticle } from '../actions'
|
||||
import aceModes from 'boost/ace-modes'
|
||||
import Select from 'react-select'
|
||||
import linkState from 'boost/linkState'
|
||||
import api from 'boost/api'
|
||||
|
||||
var modeOptions = aceModes.map(function (mode) {
|
||||
return {
|
||||
@@ -16,16 +17,27 @@ var modeOptions = aceModes.map(function (mode) {
|
||||
}
|
||||
})
|
||||
|
||||
function makeInstantArticle (article) {
|
||||
let instantArticle = Object.assign({}, article)
|
||||
instantArticle.Tags = instantArticle.Tags.map(tag => tag.name)
|
||||
return instantArticle
|
||||
}
|
||||
|
||||
export default class ArticleDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
article: Object.assign({}, props.activeArticle)
|
||||
article: makeInstantArticle(props.activeArticle)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.setState({article: nextProps.activeArticle})
|
||||
if (nextProps.activeArticle != null && nextProps.activeArticle.id !== this.state.article.id) {
|
||||
this.setState({article: makeInstantArticle(nextProps.activeArticle)}, function () {
|
||||
console.log('receive props')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
renderEmpty () {
|
||||
@@ -36,12 +48,46 @@ export default class ArticleDetail extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
handleEditButtonClick (e) {
|
||||
let { dispatch } = this.props
|
||||
dispatch(switchMode(EDIT_MODE))
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (e) {
|
||||
this.setState({openDeleteConfirmMenu: true})
|
||||
}
|
||||
|
||||
handleDeleteConfirmButtonClick (e) {
|
||||
let { dispatch, activeUser, activeArticle } = this.props
|
||||
|
||||
api.destroyArticle(activeArticle.id)
|
||||
.then(res => {
|
||||
console.log(res.body)
|
||||
})
|
||||
.catch(err => {
|
||||
// connect failed need to queue data
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
return
|
||||
}
|
||||
|
||||
if (err.status != null) throw err
|
||||
else console.log(err)
|
||||
})
|
||||
|
||||
dispatch(destroyArticle(activeUser.id, activeArticle.id))
|
||||
this.setState({openDeleteConfirmMenu: false})
|
||||
}
|
||||
|
||||
handleDeleteCancleButtonClick (e) {
|
||||
this.setState({openDeleteConfirmMenu: false})
|
||||
}
|
||||
|
||||
renderIdle () {
|
||||
let { status, activeArticle, activeUser } = this.props
|
||||
let { activeArticle, activeUser } = this.props
|
||||
|
||||
let tags = activeArticle.Tags.length > 0 ? activeArticle.Tags.map(tag => {
|
||||
return (
|
||||
<a key={tag.id}>{tag.name}</a>
|
||||
<a key={tag.name}>{tag.name}</a>
|
||||
)
|
||||
}) : (
|
||||
<span className='noTags'>Not tagged yet</span>
|
||||
@@ -50,24 +96,37 @@ export default class ArticleDetail extends React.Component {
|
||||
let folderName = folder != null ? folder.name : '(unknown)'
|
||||
|
||||
return (
|
||||
<div className='ArticleDetail show'>
|
||||
<div className='detailInfo'>
|
||||
<div className='left'>
|
||||
<div className='info'>
|
||||
<i className='fa fa-fw fa-square'/> {folderName}
|
||||
by {activeArticle.User.profileName}
|
||||
Created {moment(activeArticle.createdAt).format('YYYY/MM/DD')}
|
||||
Updated {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
|
||||
<div className='ArticleDetail idle'>
|
||||
{this.state.openDeleteConfirmMenu
|
||||
? (
|
||||
<div className='deleteConfirm'>
|
||||
<div className='right'>
|
||||
Are you sure to delete this article?
|
||||
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'><i className='fa fa-fw fa-check'/> Sure</button>
|
||||
<button onClick={e => this.handleDeleteCancleButtonClick(e)}><i className='fa fa-fw fa-times'/> Cancle</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='detailInfo'>
|
||||
<div className='left'>
|
||||
<div className='info'>
|
||||
<i className='fa fa-fw fa-square'/> {folderName}
|
||||
by {activeArticle.User.profileName}
|
||||
Created {moment(activeArticle.createdAt).format('YYYY/MM/DD')}
|
||||
Updated {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
|
||||
</div>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
<div className='right'>
|
||||
<button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button>
|
||||
<button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-trash'/></button>
|
||||
<button><i className='fa fa-fw fa-share-alt'/></button>
|
||||
</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'>
|
||||
@@ -86,7 +145,67 @@ export default class ArticleDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveButtonClick (e) {
|
||||
console.log(this.state.article)
|
||||
let { activeArticle } = this.props
|
||||
|
||||
if (typeof activeArticle.id === 'string') this.saveAsNew()
|
||||
else this.save()
|
||||
}
|
||||
|
||||
saveAsNew () {
|
||||
let { dispatch, activeUser } = this.props
|
||||
let article = this.state.article
|
||||
let newArticle = Object.assign({}, article)
|
||||
article.tags = article.Tags
|
||||
|
||||
api.createArticle(article)
|
||||
.then(res => {
|
||||
console.log(res.body)
|
||||
})
|
||||
.catch(err => {
|
||||
// connect failed need to queue data
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
return
|
||||
}
|
||||
|
||||
if (err.status != null) throw err
|
||||
else console.log(err)
|
||||
})
|
||||
|
||||
newArticle.status = UNSYNCED
|
||||
newArticle.Tags = newArticle.Tags.map(tag => { return {name: tag} })
|
||||
|
||||
dispatch(updateArticle(activeUser.id, newArticle))
|
||||
dispatch(switchMode(IDLE_MODE))
|
||||
dispatch(switchArticle(article.id))
|
||||
}
|
||||
|
||||
save () {
|
||||
let { dispatch, activeUser } = this.props
|
||||
let article = this.state.article
|
||||
let newArticle = Object.assign({}, article)
|
||||
|
||||
article.tags = article.Tags
|
||||
|
||||
api.saveArticle(article)
|
||||
.then(res => {
|
||||
console.log(res.body)
|
||||
})
|
||||
.catch(err => {
|
||||
// connect failed need to queue data
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
return
|
||||
}
|
||||
|
||||
if (err.status != null) throw err
|
||||
else console.log(err)
|
||||
})
|
||||
|
||||
newArticle.status = UNSYNCED
|
||||
newArticle.Tags = newArticle.Tags.map(tag => { return {name: tag} })
|
||||
|
||||
dispatch(updateArticle(activeUser.id, newArticle))
|
||||
dispatch(switchMode(IDLE_MODE))
|
||||
dispatch(switchArticle(article.id))
|
||||
}
|
||||
|
||||
handleFolderIdChange (value) {
|
||||
@@ -121,7 +240,7 @@ export default class ArticleDetail extends React.Component {
|
||||
}
|
||||
|
||||
renderEdit () {
|
||||
let { status, activeUser } = this.props
|
||||
let { activeUser } = this.props
|
||||
|
||||
let folderOptions = activeUser.Folders.map(folder => {
|
||||
return {
|
||||
@@ -146,7 +265,7 @@ export default class ArticleDetail extends React.Component {
|
||||
<div className='detailPanel'>
|
||||
<div className='header'>
|
||||
<div className='title'>
|
||||
<input ref='title' valueLink={this.linkState('article.title')}/>
|
||||
<input placeholder='Title' ref='title' valueLink={this.linkState('article.title')}/>
|
||||
</div>
|
||||
<Select ref='mode' onChange={value => this.handleModeChange(value)} clearable={false} options={modeOptions}placeholder='select mode...' value={this.state.article.mode} className='mode'/>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,23 @@ import React, { PropTypes } from 'react'
|
||||
import ProfileImage from 'boost/components/ProfileImage'
|
||||
import ModeIcon from 'boost/components/ModeIcon'
|
||||
import moment from 'moment'
|
||||
import { IDLE_MODE, CREATE_MODE, EDIT_MODE, NEW } from '../actions'
|
||||
import { IDLE_MODE, CREATE_MODE, EDIT_MODE, switchArticle, NEW } from '../actions'
|
||||
|
||||
export default class ArticleList extends React.Component {
|
||||
handleArticleClick (id) {
|
||||
let { dispatch } = this.props
|
||||
return function (e) {
|
||||
dispatch(switchArticle(id))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { status, articles, activeArticle } = 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>
|
||||
<a key={tag.name}>{tag.name}</a>
|
||||
)
|
||||
}) : (
|
||||
<span>Not tagged yet</span>
|
||||
@@ -19,7 +26,7 @@ export default class ArticleList extends React.Component {
|
||||
|
||||
return (
|
||||
<div key={'article-' + article.id}>
|
||||
<div className={'articleItem' + (activeArticle.id === article.id ? ' active' : '')}>
|
||||
<div onClick={e => this.handleArticleClick(article.id)(e)} className={'articleItem' + (activeArticle.id === article.id ? ' active' : '')}>
|
||||
<div className='top'>
|
||||
<i className='fa fa-fw fa-square'/>
|
||||
by <ProfileImage className='profileImage' size='20' email={article.User.email}/> {article.User.profileName}
|
||||
@@ -48,5 +55,6 @@ export default class ArticleList extends React.Component {
|
||||
ArticleList.propTypes = {
|
||||
status: PropTypes.shape(),
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape()
|
||||
activeArticle: PropTypes.shape(),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
// Action types
|
||||
export const USER_UPDATE = 'USER_UPDATE'
|
||||
export const ARTICLE_REFRESH = 'ARTICLE_REFRESH'
|
||||
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
|
||||
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
|
||||
|
||||
export const SWITCH_USER = 'SWITCH_USER'
|
||||
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
|
||||
export const SWITCH_MODE = 'SWITCH_MODE'
|
||||
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
|
||||
|
||||
// Status - mode
|
||||
export const IDLE_MODE = 'IDLE_MODE'
|
||||
export const CREATE_MODE = 'CREATE_MODE'
|
||||
export const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
// Article status
|
||||
export const NEW = 'NEW'
|
||||
export const SYNCING = 'SYNCING'
|
||||
export const UNSYNCED = 'UNSYNCED'
|
||||
|
||||
// DB
|
||||
export function updateUser (user) {
|
||||
return {
|
||||
type: USER_UPDATE,
|
||||
@@ -19,13 +27,28 @@ export function updateUser (user) {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateArticles (userId, articles) {
|
||||
export function refreshArticles (userId, articles) {
|
||||
return {
|
||||
type: ARTICLE_UPDATE,
|
||||
type: ARTICLE_REFRESH,
|
||||
data: {userId, articles}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateArticle (userId, article) {
|
||||
return {
|
||||
type: ARTICLE_UPDATE,
|
||||
data: {userId, article}
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyArticle (userId, articleId) {
|
||||
return {
|
||||
type: ARTICLE_DESTROY,
|
||||
data: { userId, articleId }
|
||||
}
|
||||
}
|
||||
|
||||
// Nav
|
||||
export function switchUser (userId) {
|
||||
return {
|
||||
type: SWITCH_USER,
|
||||
@@ -46,3 +69,10 @@ export function switchMode (mode) {
|
||||
data: mode
|
||||
}
|
||||
}
|
||||
|
||||
export function switchArticle (articleId) {
|
||||
return {
|
||||
type: SWITCH_ARTICLE,
|
||||
data: articleId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { createStore } from 'redux'
|
||||
import { Provider } from 'react-redux'
|
||||
import { updateUser, updateArticles } from './actions'
|
||||
import { updateUser, refreshArticles } from './actions'
|
||||
import reducer from './reducer'
|
||||
import { fetchCurrentUser, fetchArticles } from 'boost/api'
|
||||
import { Router, Route, IndexRoute } from 'react-router'
|
||||
@@ -37,7 +37,7 @@ let finalCreateStore = compose(devTools(), persistState(window.location.href.mat
|
||||
let store = finalCreateStore(reducer)
|
||||
let devEl = (
|
||||
<DebugPanel top right bottom>
|
||||
<DevTools store={store} monitor={LogMonitor} />
|
||||
<DevTools store={store} monitor={LogMonitor} visibleOnLoad={false}/>
|
||||
</DebugPanel>
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ React.render((
|
||||
users.forEach(user => {
|
||||
fetchArticles(user.id)
|
||||
.then(res => {
|
||||
store.dispatch(updateArticles(user.id, res.body))
|
||||
store.dispatch(refreshArticles(user.id, res.body))
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status == null) throw err
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, USER_UPDATE, ARTICLE_UPDATE, IDLE_MODE, CREATE_MODE, EDIT_MODE } from './actions'
|
||||
import { findIndex } from 'lodash'
|
||||
import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, SWITCH_ARTICLE, USER_UPDATE, ARTICLE_REFRESH, ARTICLE_UPDATE, ARTICLE_DESTROY, IDLE_MODE, CREATE_MODE } from './actions'
|
||||
|
||||
const initialCurrentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
const initialStatus = {
|
||||
@@ -29,14 +30,20 @@ function status (state, action) {
|
||||
switch (action.type) {
|
||||
case SWITCH_USER:
|
||||
state.userId = action.data
|
||||
console.log(action)
|
||||
state.mode = IDLE_MODE
|
||||
state.folderId = null
|
||||
return state
|
||||
case SWITCH_FOLDER:
|
||||
state.folderId = action.data
|
||||
state.mode = IDLE_MODE
|
||||
return state
|
||||
case SWITCH_MODE:
|
||||
state.mode = action.data
|
||||
if (state.mode === CREATE_MODE) state.articleId = null
|
||||
return state
|
||||
case SWITCH_ARTICLE:
|
||||
state.articleId = action.data
|
||||
state.mode = IDLE_MODE
|
||||
return state
|
||||
default:
|
||||
if (state == null) return initialStatus
|
||||
@@ -44,13 +51,47 @@ function status (state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function genKey (id) {
|
||||
return 'team-' + id
|
||||
}
|
||||
|
||||
function articles (state, action) {
|
||||
switch (action.type) {
|
||||
case ARTICLE_REFRESH:
|
||||
{
|
||||
let { userId, articles } = action.data
|
||||
let teamKey = genKey(userId)
|
||||
localStorage.setItem(teamKey, JSON.stringify(articles))
|
||||
state[teamKey] = articles
|
||||
}
|
||||
|
||||
return state
|
||||
case ARTICLE_UPDATE:
|
||||
let { userId, articles } = action.data
|
||||
let teamKey = 'team-' + userId
|
||||
localStorage.setItem(teamKey, JSON.stringify(articles))
|
||||
state[teamKey] = articles
|
||||
{
|
||||
let { userId, article } = action.data
|
||||
let teamKey = genKey(userId)
|
||||
let articles = JSON.parse(localStorage.getItem(teamKey))
|
||||
|
||||
let targetIndex = findIndex(articles, _article => article.id === _article.id)
|
||||
if (targetIndex < 0) articles.unshift(article)
|
||||
else articles.splice(targetIndex, 1, article)
|
||||
|
||||
localStorage.setItem(teamKey, JSON.stringify(articles))
|
||||
state[teamKey] = articles
|
||||
}
|
||||
return state
|
||||
case ARTICLE_DESTROY:
|
||||
{
|
||||
let { userId, articleId } = action.data
|
||||
let teamKey = genKey(userId)
|
||||
let articles = JSON.parse(localStorage.getItem(teamKey))
|
||||
|
||||
let targetIndex = findIndex(articles, _article => articleId === _article.id)
|
||||
if (targetIndex >= 0) articles.splice(targetIndex, 1)
|
||||
|
||||
localStorage.setItem(teamKey, JSON.stringify(articles))
|
||||
state[teamKey] = articles
|
||||
}
|
||||
return state
|
||||
default:
|
||||
if (state == null) return initialArticles
|
||||
|
||||
@@ -10,6 +10,31 @@ noTagsColor = #999
|
||||
border-left 1px solid borderColor
|
||||
*
|
||||
-webkit-user-select all
|
||||
.deleteConfirm
|
||||
width 100%
|
||||
height 70px
|
||||
.right
|
||||
float right
|
||||
button
|
||||
cursor pointer
|
||||
height 33px
|
||||
padding 0 10px
|
||||
margin-left 5px
|
||||
font-size 14px
|
||||
color inactiveTextColor
|
||||
background-color darken(white, 5%)
|
||||
border solid 1px borderColor
|
||||
border-radius 5px
|
||||
&:hover
|
||||
background-color white
|
||||
&.primary
|
||||
border none
|
||||
background-color brandColor
|
||||
color white
|
||||
&:hover
|
||||
color white
|
||||
background-color lighten(brandColor, 10%)
|
||||
|
||||
.detailInfo
|
||||
height 70px
|
||||
width 100%
|
||||
@@ -116,7 +141,7 @@ noTagsColor = #999
|
||||
font-size 32px
|
||||
font-weight bold
|
||||
outline none
|
||||
&.show
|
||||
&.idle
|
||||
.detailInfo
|
||||
.left
|
||||
right 99px
|
||||
|
||||
41
lib/api.js
41
lib/api.js
@@ -29,6 +29,32 @@ export function fetchArticles (userId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function createArticle (input) {
|
||||
return request
|
||||
.post(apiUrl + 'articles/')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
}
|
||||
|
||||
export function saveArticle (input) {
|
||||
return request
|
||||
.put(apiUrl + 'articles/' + input.id)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
}
|
||||
|
||||
export function destroyArticle (articleId) {
|
||||
return request
|
||||
.del(apiUrl + 'articles/' + articleId)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
}
|
||||
|
||||
export function createTeam (input) {
|
||||
return request
|
||||
.post(apiUrl + 'teams')
|
||||
@@ -70,3 +96,18 @@ export function sendEmail (input) {
|
||||
})
|
||||
.send(input)
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
signup,
|
||||
fetchCurrentUser,
|
||||
fetchArticles,
|
||||
createArticle,
|
||||
saveArticle,
|
||||
destroyArticle,
|
||||
createTeam,
|
||||
searchUser,
|
||||
setMember,
|
||||
deleteMember,
|
||||
sendEmail
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user