Add support for removing words from user dictionaries to admin panel (#2371)

* Add support for deletion of words from user dictionary to admin-panel

Co-authored-by: Jessica Lawshe <jessica.lawshe@overleaf.com>

* Add confirmation modal to dictionary word deletion

* Improve dictionary view with some helpful text

* Add MockSpellingApi

* Handle errors more cleanly in SpellingHandler

GitOrigin-RevId: a7d7f8bad120a15b0eaa7d77b5ee804998477ed1
This commit is contained in:
Simon Detheridge 2019-11-20 11:35:55 +00:00 committed by sharelatex
parent e4d180955f
commit 81103c93e6
3 changed files with 282 additions and 0 deletions

View file

@ -0,0 +1,94 @@
const request = require('request')
const Settings = require('settings-sharelatex')
const OError = require('@overleaf/o-error')
const TIMEOUT = 10 * 1000
module.exports = {
getUserDictionary(userId, callback) {
const url = `${Settings.apis.spelling.url}/user/${userId}`
request.get({ url: url, timeout: TIMEOUT }, (error, response) => {
if (error) {
return callback(
new OError({
message: 'error getting user dictionary',
info: { error, userId }
}).withCause(error)
)
}
if (response.statusCode < 200 || response.statusCode >= 300) {
return callback(
new OError({
message:
'Non-success code from spelling API when getting user dictionary',
info: { userId, statusCode: response.statusCode }
})
)
}
callback(null, JSON.parse(response.body))
})
},
deleteWordFromUserDictionary(userId, word, callback) {
const url = `${Settings.apis.spelling.url}/user/${userId}/unlearn`
request.post(
{
url: url,
json: {
word
},
timeout: TIMEOUT
},
(error, response) => {
if (error) {
return callback(
new OError({
message: 'error deleting word from user dictionary',
info: { error, userId, word }
}).withCause(error)
)
}
if (response.statusCode < 200 || response.statusCode >= 300) {
return callback(
new OError({
message:
'Non-success code from spelling API when removing word from user dictionary',
info: { userId, word, statusCode: response.statusCode }
})
)
}
callback()
}
)
},
deleteUserDictionary(userId, callback) {
const url = `${Settings.apis.spelling.url}/user/${userId}`
request.delete({ url: url, timeout: TIMEOUT }, (error, response) => {
if (error) {
return callback(
new OError({
message: 'error deleting user dictionary',
info: { userId }
}).withCause(error)
)
}
if (response.statusCode < 200 || response.statusCode >= 300) {
return callback(
new OError({
message:
'Non-success code from spelling API when removing user dictionary',
info: { userId, statusCode: response.statusCode }
})
)
}
callback()
})
}
}

View file

@ -0,0 +1,59 @@
const express = require('express')
const app = express()
const MockSpellingApi = {
words: {},
run() {
app.get('/user/:userId', (req, res) => {
const { userId } = req.params
const words = this.words[userId] || []
res.json(words)
})
app.delete('/user/:userId', (req, res) => {
const { userId } = req.params
this.words.delete(userId)
res.sendStatus(200)
})
app.post('/user/:userId/learn', (req, res) => {
const word = req.body.word
const { userId } = req.params
if (word) {
this.words[userId] = this.words[userId] || []
if (!this.words[userId].includes(word)) {
this.words[userId].push(word)
}
}
res.sendStatus(200)
})
app.post('/user/:userId/unlearn', (req, res) => {
const word = req.body.word
const { userId } = req.params
if (word && this.words[userId]) {
const wordIndex = this.words[userId].indexOf(word)
if (wordIndex !== -1) {
this.words[userId].splice(wordIndex, 1)
}
}
res.sendStatus(200)
})
app
.listen(3005, error => {
if (error) {
throw error
}
})
.on('error', error => {
console.error('error starting MockSpellingApi:', error.message)
process.exit(1)
})
}
}
MockSpellingApi.run()
module.exports = MockSpellingApi

View file

@ -0,0 +1,129 @@
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/Spelling/SpellingHandler.js'
)
const TIMEOUT = 1000 * 10
const SPELLING_HOST = 'http://spelling.service.test'
const SPELLING_URL = 'http://spelling.service.test'
describe('SpellingHandler', function() {
let userId, word, dictionary, dictionaryString, request, SpellingHandler
beforeEach(function() {
userId = 'wombat'
word = 'potato'
dictionary = ['wombaat', 'woombat']
dictionaryString = JSON.stringify(dictionary)
request = {
get: sinon
.stub()
.yields(null, { statusCode: 200, body: dictionaryString }),
post: sinon.stub().yields(null, { statusCode: 204 }),
delete: sinon.stub().yields(null, { statusCode: 204 })
}
SpellingHandler = SandboxedModule.require(modulePath, {
requires: {
request: request,
'logger-sharelatex': {
warn() {},
error() {},
info() {}
},
'settings-sharelatex': {
apis: { spelling: { host: SPELLING_HOST, url: SPELLING_URL } }
}
}
})
})
describe('getUserDictionary', function() {
it('calls the spelling API', function(done) {
SpellingHandler.getUserDictionary(userId, () => {
expect(request.get).to.have.been.calledWith({
url: 'http://spelling.service.test/user/wombat',
timeout: TIMEOUT
})
done()
})
})
it('returns the dictionary', function(done) {
SpellingHandler.getUserDictionary(userId, (err, dictionary) => {
expect(err).not.to.exist
expect(dictionary).to.deep.equal(dictionary)
done()
})
})
it('returns an error when the request fails', function(done) {
request.get = sinon.stub().yields(new Error('ugh'))
SpellingHandler.getUserDictionary(userId, err => {
expect(err).to.exist
done()
})
})
})
describe('deleteWordFromUserDictionary', function() {
it('calls the spelling API', function(done) {
SpellingHandler.deleteWordFromUserDictionary(userId, word, () => {
expect(request.post).to.have.been.calledWith({
url: 'http://spelling.service.test/user/wombat/unlearn',
json: {
word: word
},
timeout: TIMEOUT
})
done()
})
})
it('does not return an error', function(done) {
SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
expect(err).not.to.exist
done()
})
})
it('returns an error when the request fails', function(done) {
request.post = sinon.stub().yields(new Error('ugh'))
SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
expect(err).to.exist
done()
})
})
})
describe('deleteUserDictionary', function() {
it('calls the spelling API', function(done) {
SpellingHandler.deleteUserDictionary(userId, () => {
expect(request.delete).to.have.been.calledWith({
url: 'http://spelling.service.test/user/wombat',
timeout: TIMEOUT
})
done()
})
})
it('does not return an error', function(done) {
SpellingHandler.deleteUserDictionary(userId, err => {
expect(err).not.to.exist
done()
})
})
it('returns an error when the request fails', function(done) {
request.delete = sinon.stub().yields(new Error('ugh'))
SpellingHandler.deleteUserDictionary(userId, err => {
expect(err).to.exist
done()
})
})
})
})