Skip to content

Commit

Permalink
feat: 增加分类停用功能
Browse files Browse the repository at this point in the history
分类直接删除会导致部分已有工单分类出现错误,所以将删除改为停用。停用效果:

* 新建工单不能选择停用分类。
* 工单列表也可以展现已停用的分类。
* 工单详情页可以展现已停用的分类,但客服修改分类时不能选择已停用的分类。
* 客服工单列表页可以展现已停用的分类,客服也可以根据已停用分类查询。
* 分类列表和排序页不展示已停用分类。
  • Loading branch information
sdjcw committed Aug 3, 2018
1 parent 88747ad commit aa86664
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 125 deletions.
32 changes: 12 additions & 20 deletions api/Category.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Promise = require('bluebird')
const AV = require('leanengine')

const {getCategoriesTree, depthFirstSearchFind} = require('./common')
const errorHandler = require('./errorHandler')

AV.Cloud.beforeSave('Category', (req, res) => {
Expand All @@ -14,28 +15,19 @@ AV.Cloud.beforeSave('Category', (req, res) => {
}).catch(errorHandler.captureException)
})

AV.Cloud.beforeDelete('Category', async (req) => {
const category = req.object
const user = await new AV.Query('_User')
.equalTo('categories.objectId', category.id)
.first({user: req.currentUser})
if (user) {
throw new AV.Cloud.Error('仍有人负责该分类,不能删除。')
}

const ticket = await new AV.Query('Ticket')
.equalTo('category.objectId', category.id)
.first({user: req.currentUser})
if (ticket) {
throw new AV.Cloud.Error('仍有工单属于该分类,不能删除。')
AV.Cloud.beforeUpdate('Category', async ({object: category, currentUser: user}) => {
if (category.updatedKeys.includes('deletedAt')) {
const categoriesTree = await getCategoriesTree({user})
const node = depthFirstSearchFind(categoriesTree, c => c.id == category.id)
const enableChild = depthFirstSearchFind(node.children, c => !c.get('deletedAt'))
if (enableChild) {
throw new AV.Cloud.Error('该分类仍有未停用的子分类,不能停用。')
}
}
})

const categoryChild = await new AV.Query('Category')
.equalTo('parent', AV.Object.createWithoutData('Category', category.id))
.first({user: req.currentUser})
if (categoryChild) {
throw new AV.Cloud.Error('仍有子分类属于该分类,不能删除。')
}
AV.Cloud.beforeDelete('Category', () => {
throw new AV.Cloud.Error('不能删除分类,如果需要,请使用「停用」功能。')
})

const getCategoryAcl = () => {
Expand Down
17 changes: 12 additions & 5 deletions api/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ const hljs = require('highlight.js')
const AV = require('leanengine')

const config = require('../config')
const {getGravatarHash, getTinyCategoryInfo, getCategoryPathName} = require('../lib/common')

exports.getTinyCategoryInfo = getTinyCategoryInfo
exports.getCategoryPathName = getCategoryPathName
Object.assign(module.exports, require('../lib/common'))

exports.getTinyUserInfo = (user) => {
if (!user) {
Expand All @@ -19,15 +17,15 @@ exports.getTinyUserInfo = (user) => {
objectId: user.id,
username: user.get('username'),
name: user.get('name'),
gravatarHash: getGravatarHash(user.get('email'))
gravatarHash: exports.getGravatarHash(user.get('email'))
})
}
return user.fetch().then((user) => {
return {
objectId: user.id,
username: user.get('username'),
name: user.get('name'),
gravatarHash: getGravatarHash(user.get('email'))
gravatarHash: exports.getGravatarHash(user.get('email'))
}
})
}
Expand Down Expand Up @@ -112,3 +110,12 @@ exports.forEachAVObject = (query, fn, authOptions) => {
return innerFn()
}

exports.getCategoriesTree = (authOptions) => {
return new AV.Query('Category')
.descending('createdAt')
.find(authOptions)
.then(categories => {
return exports.makeTree(categories)
})
}

50 changes: 46 additions & 4 deletions lib/common.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const crypto = require('crypto')
const _ = require('lodash')

exports.TICKET_STATUS = {
// 0~99 未开始处理
Expand Down Expand Up @@ -48,13 +49,54 @@ exports.getGravatarHash = (email) => {
return shasum.digest('hex')
}

exports.makeTree = (objs) => {
const sortFunc = (o) => {
return o.get('order') != null ? o.get('order') : o.createdAt.getTime()
}
const innerFunc = (parents, children) => {
if (parents && children) {
parents.forEach(p => {
const [cs, others] = _.partition(children, c => c.get('parent').id == p.id)
p.children = _.sortBy(cs, sortFunc)
cs.forEach(c => c.parent = p)
innerFunc(p.children, others)
})
}
}
const [parents, children] = _.partition(objs, o => !o.get('parent'))
innerFunc(parents, children)
return _.sortBy(parents, sortFunc)
}

exports.depthFirstSearchMap = (array, fn) => {
return _.flatten(array.map((a, index, array) => {
const result = fn(a, index, array)
if (a.children) {
return [result, ...exports.depthFirstSearchMap(a.children, fn)]
}
return result
}))
}

exports.depthFirstSearchFind = (array, fn) => {
for (let i = 0; i < array.length; i++) {
const obj = array[i]
if (fn(obj)) {
return obj
}

if (obj.children) {
const finded = exports.depthFirstSearchFind(obj.children, fn)
if (finded) {
return finded
}
}
}
}

exports.getTinyCategoryInfo = (category) => {
return {
objectId: category.id,
name: category.get('name'),
}
}

exports.getCategoryPathName = (categoryPath) => {
return categoryPath.map(c => c.name || c.get('name')).join(' / ')
}
10 changes: 5 additions & 5 deletions modules/CustomerServiceTickets.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AV from 'leancloud-storage/live-query'
import css from './CustomerServiceTickets.css'
import DocumentTitle from 'react-document-title'

import {UserLabel, TicketStatusLabel, getCustomerServices, getCategoreisTree, depthFirstSearchMap, depthFirstSearchFind, getNodeIndentString, getNodePath, getTinyCategoryInfo} from './common'
import {UserLabel, TicketStatusLabel, getCustomerServices, getCategoriesTree, depthFirstSearchMap, depthFirstSearchFind, getNodeIndentString, getNodePath, getTinyCategoryInfo, getCategoryName} from './common'
import {TICKET_STATUS, TICKET_STATUS_MSG, ticketOpenedStatuses, ticketClosedStatuses} from '../lib/common'

let authorSearchTimeoutId
Expand All @@ -31,7 +31,7 @@ export default class CustomerServiceTickets extends Component {
const authorId = this.props.location.query.authorId
Promise.all([
getCustomerServices(),
getCategoreisTree(),
getCategoriesTree(false),
authorId && new AV.Query('_User').get(authorId),
])
.then(([customerServices, categoriesTree, author]) => {
Expand Down Expand Up @@ -206,7 +206,7 @@ export default class CustomerServiceTickets extends Component {
<div className={css.left}>
<Link className={css.title} to={'/tickets/' + ticket.get('nid')}>{ticket.get('title')}</Link>
{getNodePath(category).map(c => {
return <Link key={c.id} to={this.getQueryUrl({categoryId: c.id})}><span className={css.category}>{c.get('name')}</span></Link>
return <Link key={c.id} to={this.getQueryUrl({categoryId: c.id})}><span className={css.category}>{getCategoryName(c)}</span></Link>
})}
{filters.isOpen === 'true' ||
<span>{ticket.get('evaluation') && (ticket.get('evaluation').star === 1 && <span className={css.satisfaction + ' ' + css.happy}>满意</span> || <span className={css.satisfaction + ' ' + css.unhappy}>不满意</span>)}</span>
Expand Down Expand Up @@ -249,7 +249,7 @@ export default class CustomerServiceTickets extends Component {
return <MenuItem key={user.id} eventKey={user.id}>{user.get('username')}</MenuItem>
})
const categoryMenuItems = depthFirstSearchMap(this.state.categoriesTree, c => {
return <MenuItem key={c.id} eventKey={c.id}>{getNodeIndentString(c) + c.get('name')}</MenuItem>
return <MenuItem key={c.id} eventKey={c.id}>{getNodeIndentString(c) + getCategoryName(c)}</MenuItem>
})

let statusTitle
Expand Down Expand Up @@ -279,7 +279,7 @@ export default class CustomerServiceTickets extends Component {
if (filters.categoryId) {
const category = depthFirstSearchFind(this.state.categoriesTree, c => c.id === filters.categoryId)
if (category) {
categoryTitle = category.get('name')
categoryTitle = getCategoryName(category)
} else {
categoryTitle = 'categoryId 错误'
}
Expand Down
4 changes: 2 additions & 2 deletions modules/NewTicket.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {FormGroup, ControlLabel, FormControl, Button, Tooltip, OverlayTrigger} f
import AV from 'leancloud-storage/live-query'

import TextareaWithPreview from './components/TextareaWithPreview'
const {uploadFiles, getCategoreisTree, depthFirstSearchFind, getTinyCategoryInfo} = require('./common')
import {uploadFiles, getCategoriesTree, depthFirstSearchFind, getTinyCategoryInfo} from './common'

export default class NewTicket extends React.Component {

Expand All @@ -23,7 +23,7 @@ export default class NewTicket extends React.Component {

componentDidMount() {
this.contentTextarea.addEventListener('paste', this.pasteEventListener.bind(this))
return getCategoreisTree()
return getCategoriesTree()
.then(categoriesTree => {
let {
title=(localStorage.getItem('ticket:new:title') || ''),
Expand Down
4 changes: 2 additions & 2 deletions modules/Ticket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'
import {FormGroup, ControlLabel, FormControl, Label, Alert, Button, ButtonToolbar, Radio, Tooltip, OverlayTrigger} from 'react-bootstrap'
import AV from 'leancloud-storage/live-query'

import {UserLabel, TicketStatusLabel, uploadFiles, getCategoryPathName, getCategoreisTree, getTinyCategoryInfo} from './common'
import {UserLabel, TicketStatusLabel, uploadFiles, getCategoryPathName, getCategoriesTree, getTinyCategoryInfo} from './common'
import UpdateTicket from './UpdateTicket'
import TextareaWithPreview from './components/TextareaWithPreview'
import css from './Ticket.css'
Expand Down Expand Up @@ -52,7 +52,7 @@ export default class Ticket extends Component {
}

return Promise.all([
getCategoreisTree(),
getCategoriesTree(false),
this.getReplyQuery(ticket).find(),
new AV.Query('Tag').equalTo('ticket', ticket).find(),
this.getOpsLogQuery(ticket).find(),
Expand Down
4 changes: 2 additions & 2 deletions modules/Tickets.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import AV from 'leancloud-storage/live-query'
import css from './CustomerServiceTickets.css'
import DocumentTitle from 'react-document-title'

import {UserLabel, TicketStatusLabel, getCategoryPathName, getCategoreisTree} from './common'
import {UserLabel, TicketStatusLabel, getCategoryPathName, getCategoriesTree} from './common'

export default class Tickets extends Component {

Expand All @@ -25,7 +25,7 @@ export default class Tickets extends Component {
}

componentDidMount () {
getCategoreisTree()
getCategoriesTree(false)
.then(categoriesTree => {
this.setState({categoriesTree})
this.findTickets({})
Expand Down
2 changes: 1 addition & 1 deletion modules/UpdateTicket.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default class UpdateTicket extends Component {
<ControlLabel>修改类别</ControlLabel>
<CategoriesSelect categoriesTree={this.props.categoriesTree}
selected={this.props.ticket.get('category')}
onChange={this.handleCategoryChange.bind(this)}/>
onChange={this.handleCategoryChange.bind(this)} />
</FormGroup>
</div>
}
Expand Down
Loading

0 comments on commit aa86664

Please sign in to comment.