mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 08:03:42 -05:00
Merge pull request #16545 from overleaf/dp-project-owner-delete-labels
Allow project owners to delete history labels GitOrigin-RevId: 16111337681ac4085db2cf48e9d4c2fa87993b77
This commit is contained in:
parent
4683cad1cb
commit
e3513a9d50
11 changed files with 198 additions and 15 deletions
|
@ -379,13 +379,29 @@ export function createLabel(req, res, next) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteLabel(req, res, next) {
|
/**
|
||||||
|
* This will delete a label if it is owned by the current user. If you wish to
|
||||||
|
* delete a label regardless of the current user, then use `deleteLabel` instead.
|
||||||
|
*/
|
||||||
|
export function deleteLabelForUser(req, res, next) {
|
||||||
const {
|
const {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
label_id: labelId,
|
label_id: labelId,
|
||||||
} = req.params
|
} = req.params
|
||||||
LabelsManager.deleteLabel(projectId, userId, labelId, error => {
|
|
||||||
|
LabelsManager.deleteLabelForUser(projectId, userId, labelId, error => {
|
||||||
|
if (error != null) {
|
||||||
|
return next(error)
|
||||||
|
}
|
||||||
|
res.sendStatus(204)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLabel(req, res, next) {
|
||||||
|
const { project_id: projectId, label_id: labelId } = req.params
|
||||||
|
|
||||||
|
LabelsManager.deleteLabel(projectId, labelId, error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export function createLabel(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteLabel(projectId, userId, labelId, callback) {
|
export function deleteLabelForUser(projectId, userId, labelId, callback) {
|
||||||
return _toObjectId(
|
return _toObjectId(
|
||||||
projectId,
|
projectId,
|
||||||
userId,
|
userId,
|
||||||
|
@ -102,6 +102,21 @@ export function deleteLabel(projectId, userId, labelId, callback) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteLabel(projectId, labelId, callback) {
|
||||||
|
return _toObjectId(projectId, labelId, function (error, projectId, labelId) {
|
||||||
|
if (error != null) {
|
||||||
|
return callback(OError.tag(error))
|
||||||
|
}
|
||||||
|
return db.projectHistoryLabels.deleteOne(
|
||||||
|
{
|
||||||
|
_id: new ObjectId(labelId),
|
||||||
|
project_id: new ObjectId(projectId),
|
||||||
|
},
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function transferLabels(fromUserId, toUserId, callback) {
|
export function transferLabels(fromUserId, toUserId, callback) {
|
||||||
return _toObjectId(
|
return _toObjectId(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
|
@ -112,6 +112,24 @@ export function initialize(app) {
|
||||||
|
|
||||||
app.delete(
|
app.delete(
|
||||||
'/project/:project_id/user/:user_id/labels/:label_id',
|
'/project/:project_id/user/:user_id/labels/:label_id',
|
||||||
|
validate({
|
||||||
|
params: Joi.object({
|
||||||
|
project_id: Joi.string().regex(/^[0-9a-f]{24}$/),
|
||||||
|
user_id: Joi.string().regex(/^[0-9a-f]{24}$/),
|
||||||
|
label_id: Joi.string().regex(/^[0-9a-f]{24}$/),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
HttpController.deleteLabelForUser
|
||||||
|
)
|
||||||
|
|
||||||
|
app.delete(
|
||||||
|
'/project/:project_id/labels/:label_id',
|
||||||
|
validate({
|
||||||
|
params: Joi.object({
|
||||||
|
project_id: Joi.string().regex(/^[0-9a-f]{24}$/),
|
||||||
|
label_id: Joi.string().regex(/^[0-9a-f]{24}$/),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
HttpController.deleteLabel
|
HttpController.deleteLabel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,40 @@ describe('Labels', function () {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
return ProjectHistoryClient.deleteLabel(
|
return ProjectHistoryClient.deleteLabel(
|
||||||
|
this.project_id,
|
||||||
|
label.id,
|
||||||
|
error => {
|
||||||
|
if (error != null) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return ProjectHistoryClient.getLabels(
|
||||||
|
this.project_id,
|
||||||
|
(error, labels) => {
|
||||||
|
if (error != null) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
expect(labels).to.deep.equal([])
|
||||||
|
return done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can delete labels for the current user', function (done) {
|
||||||
|
return ProjectHistoryClient.createLabel(
|
||||||
|
this.project_id,
|
||||||
|
this.user_id,
|
||||||
|
7,
|
||||||
|
this.comment,
|
||||||
|
this.created_at,
|
||||||
|
(error, label) => {
|
||||||
|
if (error != null) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return ProjectHistoryClient.deleteLabelForUser(
|
||||||
this.project_id,
|
this.project_id,
|
||||||
this.user_id,
|
this.user_id,
|
||||||
label.id,
|
label.id,
|
||||||
|
|
|
@ -234,7 +234,7 @@ export function getLabels(projectId, callback) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteLabel(projectId, userId, labelId, callback) {
|
export function deleteLabelForUser(projectId, userId, labelId, callback) {
|
||||||
request.delete(
|
request.delete(
|
||||||
{
|
{
|
||||||
url: `http://localhost:3054/project/${projectId}/user/${userId}/labels/${labelId}`,
|
url: `http://localhost:3054/project/${projectId}/user/${userId}/labels/${labelId}`,
|
||||||
|
@ -249,6 +249,21 @@ export function deleteLabel(projectId, userId, labelId, callback) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteLabel(projectId, labelId, callback) {
|
||||||
|
request.delete(
|
||||||
|
{
|
||||||
|
url: `http://localhost:3054/project/${projectId}/labels/${labelId}`,
|
||||||
|
},
|
||||||
|
(error, res, body) => {
|
||||||
|
if (error) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
expect(res.statusCode).to.equal(204)
|
||||||
|
callback(null, body)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function setFailure(failureEntry, callback) {
|
export function setFailure(failureEntry, callback) {
|
||||||
db.projectHistoryFailures.deleteOne(
|
db.projectHistoryFailures.deleteOne(
|
||||||
{ project_id: { $exists: true } },
|
{ project_id: { $exists: true } },
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe('HttpController', function () {
|
||||||
this.LabelsManager = {
|
this.LabelsManager = {
|
||||||
createLabel: sinon.stub(),
|
createLabel: sinon.stub(),
|
||||||
deleteLabel: sinon.stub().yields(),
|
deleteLabel: sinon.stub().yields(),
|
||||||
|
deleteLabelForUser: sinon.stub().yields(),
|
||||||
getLabels: sinon.stub(),
|
getLabels: sinon.stub(),
|
||||||
}
|
}
|
||||||
this.HistoryApiManager = {
|
this.HistoryApiManager = {
|
||||||
|
@ -75,6 +76,7 @@ describe('HttpController', function () {
|
||||||
})
|
})
|
||||||
this.pathname = 'doc-id-123'
|
this.pathname = 'doc-id-123'
|
||||||
this.projectId = new ObjectId().toString()
|
this.projectId = new ObjectId().toString()
|
||||||
|
this.projectOwnerId = new ObjectId().toString()
|
||||||
this.next = sinon.stub()
|
this.next = sinon.stub()
|
||||||
this.userId = new ObjectId().toString()
|
this.userId = new ObjectId().toString()
|
||||||
this.now = Date.now()
|
this.now = Date.now()
|
||||||
|
@ -473,7 +475,7 @@ describe('HttpController', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleteLabel', function () {
|
describe('deleteLabelForUser', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.req = {
|
this.req = {
|
||||||
params: {
|
params: {
|
||||||
|
@ -482,12 +484,34 @@ describe('HttpController', function () {
|
||||||
label_id: (this.label_id = new ObjectId()),
|
label_id: (this.label_id = new ObjectId()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
this.HttpController.deleteLabelForUser(this.req, this.res, this.next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete a label for a project', function () {
|
||||||
|
this.LabelsManager.deleteLabelForUser
|
||||||
|
.calledWith(this.projectId, this.userId, this.label_id)
|
||||||
|
.should.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 204', function () {
|
||||||
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deleteLabel', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.req = {
|
||||||
|
params: {
|
||||||
|
project_id: this.projectId,
|
||||||
|
label_id: (this.label_id = new ObjectId()),
|
||||||
|
},
|
||||||
|
}
|
||||||
this.HttpController.deleteLabel(this.req, this.res, this.next)
|
this.HttpController.deleteLabel(this.req, this.res, this.next)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create a label for a project', function () {
|
it('should delete a label for a project', function () {
|
||||||
this.LabelsManager.deleteLabel
|
this.LabelsManager.deleteLabel
|
||||||
.calledWith(this.projectId, this.userId, this.label_id)
|
.calledWith(this.projectId, this.label_id)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -232,10 +232,10 @@ describe('LabelsManager', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return describe('deleteLabel', function () {
|
describe('deleteLabelForUser', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.db.projectHistoryLabels.deleteOne.yields()
|
this.db.projectHistoryLabels.deleteOne.yields()
|
||||||
return this.LabelsManager.deleteLabel(
|
return this.LabelsManager.deleteLabelForUser(
|
||||||
this.project_id,
|
this.project_id,
|
||||||
this.user_id,
|
this.user_id,
|
||||||
this.label_id,
|
this.label_id,
|
||||||
|
@ -256,4 +256,27 @@ describe('LabelsManager', function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return describe('deleteLabel', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.db.projectHistoryLabels.deleteOne.yields()
|
||||||
|
return this.LabelsManager.deleteLabel(
|
||||||
|
this.project_id,
|
||||||
|
this.label_id,
|
||||||
|
this.callback
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return it('removes the label from the database', function () {
|
||||||
|
return expect(
|
||||||
|
this.db.projectHistoryLabels.deleteOne
|
||||||
|
).to.have.been.calledWith(
|
||||||
|
{
|
||||||
|
_id: new ObjectId(this.label_id),
|
||||||
|
project_id: new ObjectId(this.project_id),
|
||||||
|
},
|
||||||
|
this.callback
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,6 +6,7 @@ const request = require('request')
|
||||||
const settings = require('@overleaf/settings')
|
const settings = require('@overleaf/settings')
|
||||||
const SessionManager = require('../Authentication/SessionManager')
|
const SessionManager = require('../Authentication/SessionManager')
|
||||||
const UserGetter = require('../User/UserGetter')
|
const UserGetter = require('../User/UserGetter')
|
||||||
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
const Errors = require('../Errors/Errors')
|
const Errors = require('../Errors/Errors')
|
||||||
const HistoryManager = require('./HistoryManager')
|
const HistoryManager = require('./HistoryManager')
|
||||||
const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||||
|
@ -218,16 +219,35 @@ module.exports = HistoryController = {
|
||||||
deleteLabel(req, res, next) {
|
deleteLabel(req, res, next) {
|
||||||
const { Project_id: projectId, label_id: labelId } = req.params
|
const { Project_id: projectId, label_id: labelId } = req.params
|
||||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
HistoryController._makeRequest(
|
|
||||||
|
ProjectGetter.getProject(
|
||||||
|
projectId,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
owner_ref: true,
|
||||||
url: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels/${labelId}`,
|
|
||||||
},
|
},
|
||||||
function (err) {
|
(err, project) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err)
|
return next(err)
|
||||||
}
|
}
|
||||||
res.sendStatus(204)
|
|
||||||
|
// If the current user is the project owner, we can use the non-user-specific delete label endpoint.
|
||||||
|
// Otherwise, we have to use the user-specific version (which only deletes the label if it is owned by the user)
|
||||||
|
const deleteEndpointUrl = project.owner_ref.equals(userId)
|
||||||
|
? `${settings.apis.project_history.url}/project/${projectId}/labels/${labelId}`
|
||||||
|
: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels/${labelId}`
|
||||||
|
|
||||||
|
HistoryController._makeRequest(
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
url: deleteEndpointUrl,
|
||||||
|
},
|
||||||
|
function (err) {
|
||||||
|
if (err) {
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
res.sendStatus(204)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { isPseudoLabel } from '../../utils/label'
|
||||||
import { LoadedLabel } from '../../services/types/label'
|
import { LoadedLabel } from '../../services/types/label'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { formatTimeBasedOnYear } from '@/features/utils/format-date'
|
import { formatTimeBasedOnYear } from '@/features/utils/format-date'
|
||||||
|
import { useEditorContext } from '@/shared/context/editor-context'
|
||||||
|
|
||||||
type TagProps = {
|
type TagProps = {
|
||||||
label: LoadedLabel
|
label: LoadedLabel
|
||||||
|
@ -22,6 +23,8 @@ type TagProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tag({ label, currentUserId, ...props }: TagProps) {
|
function Tag({ label, currentUserId, ...props }: TagProps) {
|
||||||
|
const { isProjectOwner } = useEditorContext()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const { projectId } = useHistoryContext()
|
const { projectId } = useHistoryContext()
|
||||||
|
@ -67,7 +70,7 @@ function Tag({ label, currentUserId, ...props }: TagProps) {
|
||||||
prepend={<Icon type="tag" fw />}
|
prepend={<Icon type="tag" fw />}
|
||||||
onClose={showConfirmationModal}
|
onClose={showConfirmationModal}
|
||||||
closeButton={Boolean(
|
closeButton={Boolean(
|
||||||
isOwnedByCurrentUser && !isPseudoCurrentStateLabel
|
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
|
||||||
)}
|
)}
|
||||||
closeBtnProps={{ 'aria-label': t('delete') }}
|
closeBtnProps={{ 'aria-label': t('delete') }}
|
||||||
className="history-version-badge"
|
className="history-version-badge"
|
||||||
|
|
|
@ -128,6 +128,20 @@ class MockProjectHistoryApi extends AbstractMockApi {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.app.delete('/project/:projectId/labels/:labelId', (req, res) => {
|
||||||
|
const { projectId, labelId } = req.params
|
||||||
|
const label =
|
||||||
|
this.labels[projectId] != null
|
||||||
|
? this.labels[projectId][labelId]
|
||||||
|
: undefined
|
||||||
|
if (label != null) {
|
||||||
|
this.deleteLabel(projectId, labelId)
|
||||||
|
res.sendStatus(204)
|
||||||
|
} else {
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.app.post('/project/:projectId/flush', (req, res) => {
|
this.app.post('/project/:projectId/flush', (req, res) => {
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,6 +38,7 @@ describe('HistoryController', function () {
|
||||||
'../Project/ProjectEntityUpdateHandler':
|
'../Project/ProjectEntityUpdateHandler':
|
||||||
(this.ProjectEntityUpdateHandler = {}),
|
(this.ProjectEntityUpdateHandler = {}),
|
||||||
'../User/UserGetter': (this.UserGetter = {}),
|
'../User/UserGetter': (this.UserGetter = {}),
|
||||||
|
'../Project/ProjectGetter': (this.ProjectGetter = {}),
|
||||||
'./RestoreManager': (this.RestoreManager = {}),
|
'./RestoreManager': (this.RestoreManager = {}),
|
||||||
'../../infrastructure/Features': (this.Features = sinon
|
'../../infrastructure/Features': (this.Features = sinon
|
||||||
.stub()
|
.stub()
|
||||||
|
|
Loading…
Reference in a new issue