Merge pull request #2150 from overleaf/cmg-archiving-frontend

Frontend for new archiving and trashing

GitOrigin-RevId: be8676ab6c2fea7f8fb23655772a008d067b2a78
This commit is contained in:
Chrystal Maria Griffiths
2019-11-25 15:41:12 +00:00
committed by sharelatex
parent 1a456da017
commit 0abe99d98f
13 changed files with 954 additions and 579 deletions

View File

@@ -34,7 +34,6 @@ const Features = require('../../infrastructure/Features')
const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler')
const { getUserAffiliations } = require('../Institutions/InstitutionsAPI')
const V1Handler = require('../V1/V1Handler')
const { Project } = require('../../models/Project')
const ProjectController = {
_isInPercentageRollout(rolloutName, objectId, percentage) {
@@ -145,11 +144,10 @@ const ProjectController = {
archiveProject(req, res, next) {
const projectId = req.params.Project_id
const userId = AuthenticationController.getLoggedInUserId(req)
logger.log({ projectId }, 'received request to archive project')
const user = AuthenticationController.getSessionUser(req)
ProjectDeleter.archiveProject(projectId, user._id, function(err) {
ProjectDeleter.archiveProject(projectId, userId, function(err) {
if (err != null) {
return next(err)
} else {
@@ -160,11 +158,38 @@ const ProjectController = {
unarchiveProject(req, res, next) {
const projectId = req.params.Project_id
const userId = AuthenticationController.getLoggedInUserId(req)
logger.log({ projectId }, 'received request to unarchive project')
const user = AuthenticationController.getSessionUser(req)
ProjectDeleter.unarchiveProject(projectId, userId, function(err) {
if (err != null) {
return next(err)
} else {
return res.sendStatus(200)
}
})
},
ProjectDeleter.unarchiveProject(projectId, user._id, function(err) {
trashProject(req, res, next) {
const projectId = req.params.project_id
const userId = AuthenticationController.getLoggedInUserId(req)
logger.log({ projectId }, 'received request to trash project')
ProjectDeleter.trashProject(projectId, userId, function(err) {
if (err != null) {
return next(err)
} else {
return res.sendStatus(200)
}
})
},
untrashProject(req, res, next) {
const projectId = req.params.project_id
const userId = AuthenticationController.getLoggedInUserId(req)
logger.log({ projectId }, 'received request to untrash project')
ProjectDeleter.untrashProject(projectId, userId, function(err) {
if (err != null) {
return next(err)
} else {
@@ -210,38 +235,6 @@ const ProjectController = {
})
},
trashProject(req, res, next) {
const projectId = req.params.project_id
const userId = AuthenticationController.getLoggedInUserId(req)
Project.update(
{ _id: projectId },
{ $addToSet: { trashed: userId } },
error => {
if (error) {
return next(error)
}
res.sendStatus(200)
}
)
},
untrashProject(req, res, next) {
const projectId = req.params.project_id
const userId = AuthenticationController.getLoggedInUserId(req)
Project.update(
{ _id: projectId },
{ $pull: { trashed: userId } },
error => {
if (error) {
return next(error)
}
res.sendStatus(200)
}
)
},
cloneProject(req, res, next) {
res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete
metrics.inc('cloned-project')
@@ -376,7 +369,7 @@ const ProjectController = {
projects(cb) {
ProjectGetter.findAllUsersProjects(
userId,
'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens',
'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens',
cb
)
},
@@ -925,6 +918,10 @@ const ProjectController = {
},
_buildProjectViewModel(project, accessLevel, source, userId) {
const archived = ProjectHelper.isArchived(project, userId)
// If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
const trashed = ProjectHelper.isTrashed(project, userId) && !archived
TokenAccessHandler.protectTokens(project, accessLevel)
const model = {
id: project._id,
@@ -934,7 +931,8 @@ const ProjectController = {
publicAccessLevel: project.publicAccesLevel,
accessLevel,
source,
archived: ProjectHelper.isArchived(project, userId),
archived,
trashed,
owner_ref: project.owner_ref,
tokens: project.tokens,
isV1Project: false
@@ -947,11 +945,16 @@ const ProjectController = {
},
_buildV1ProjectViewModel(project) {
const archived = project.archived
// If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
const trashed = project.removed && !archived
const projectViewModel = {
id: project.id,
name: project.title,
lastUpdated: new Date(project.updated_at * 1000), // Convert from epoch
archived: project.removed || project.archived,
archived: archived,
trashed: trashed,
isV1Project: true
}
if (

View File

@@ -11,7 +11,7 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { db } = require('../../infrastructure/mongojs')
const { db, ObjectId } = require('../../infrastructure/mongojs')
const { promisify, callbackify } = require('util')
const { Project } = require('../../models/Project')
const { DeletedProject } = require('../../models/DeletedProject')
@@ -160,11 +160,11 @@ const ProjectDeleter = {
// Async methods
async function archiveProject(project_id, userId) {
logger.log({ project_id }, 'archiving project from user request')
async function archiveProject(projectId, userId) {
logger.log({ projectId }, 'archiving project from user request')
try {
let project = await Project.findOne({ _id: project_id }).exec()
let project = await Project.findOne({ _id: projectId }).exec()
if (!project) {
throw new Errors.NotFoundError('project not found')
}
@@ -174,18 +174,21 @@ async function archiveProject(project_id, userId) {
'ARCHIVE'
)
await Project.update({ _id: project_id }, { $set: { archived: archived } })
await Project.update(
{ _id: projectId },
{ $set: { archived: archived }, $pull: { trashed: ObjectId(userId) } }
)
} catch (err) {
logger.warn({ err }, 'problem archiving project')
throw err
}
}
async function unarchiveProject(project_id, userId) {
logger.log({ project_id }, 'unarchiving project from user request')
async function unarchiveProject(projectId, userId) {
logger.log({ projectId }, 'unarchiving project from user request')
try {
let project = await Project.findOne({ _id: project_id }).exec()
let project = await Project.findOne({ _id: projectId }).exec()
if (!project) {
throw new Errors.NotFoundError('project not found')
}
@@ -196,13 +199,54 @@ async function unarchiveProject(project_id, userId) {
'UNARCHIVE'
)
await Project.update({ _id: project_id }, { $set: { archived: archived } })
await Project.update({ _id: projectId }, { $set: { archived: archived } })
} catch (err) {
logger.warn({ err }, 'problem unarchiving project')
throw err
}
}
async function trashProject(projectId, userId) {
logger.log({ projectId }, 'trashing project from user request')
try {
let project = await Project.findOne({ _id: projectId }).exec()
if (!project) {
throw new Errors.NotFoundError('project not found')
}
await Project.update(
{ _id: projectId },
{
$addToSet: { trashed: ObjectId(userId) },
$pull: { archived: ObjectId(userId) }
}
)
} catch (err) {
logger.warn({ err }, 'problem trashing project')
throw err
}
}
async function untrashProject(projectId, userId) {
logger.log({ projectId }, 'untrashing project from user request')
try {
let project = await Project.findOne({ _id: projectId }).exec()
if (!project) {
throw new Errors.NotFoundError('project not found')
}
await Project.update(
{ _id: projectId },
{ $pull: { trashed: ObjectId(userId) } }
)
} catch (err) {
logger.warn({ err }, 'problem untrashing project')
throw err
}
}
async function deleteProject(project_id, options = {}) {
logger.log({ project_id }, 'deleting project')
@@ -350,6 +394,8 @@ async function expireDeletedProject(projectId) {
const promises = {
archiveProject: archiveProject,
unarchiveProject: unarchiveProject,
trashProject: trashProject,
untrashProject: untrashProject,
deleteProject: deleteProject,
undeleteProject: undeleteProject,
expireDeletedProject: expireDeletedProject,
@@ -359,6 +405,8 @@ const promises = {
ProjectDeleter.promises = promises
ProjectDeleter.archiveProject = callbackify(archiveProject)
ProjectDeleter.unarchiveProject = callbackify(unarchiveProject)
ProjectDeleter.trashProject = callbackify(trashProject)
ProjectDeleter.untrashProject = callbackify(untrashProject)
ProjectDeleter.deleteProject = callbackify(deleteProject)
ProjectDeleter.undeleteProject = callbackify(undeleteProject)
ProjectDeleter.expireDeletedProject = callbackify(expireDeletedProject)

View File

@@ -3,7 +3,6 @@ td.project-list-table-name-cell(ng-if-start="!project.isV1Project")
input.project-list-table-select-item(
select-individual,
type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
@@ -66,6 +65,7 @@ td.project-list-table-actions-cell(ng-if-end)
ng-if="!project.isTableActionInflight"
)
button.btn.btn-link.action-btn(
ng-if="!(project.archived || project.trashed)"
aria-label=translate('copy'),
tooltip=translate('copy'),
tooltip-placement="top",
@@ -82,41 +82,59 @@ td.project-list-table-actions-cell(ng-if-end)
)
i.icon.fa.fa-cloud-download(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!project.archived && isOwner()"
ng-if="!project.archived"
aria-label=translate('archive'),
tooltip=translate('archive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
ng-click="archive($event)"
)
i.icon.fa.fa-inbox(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!isOwner()"
aria-label=translate('leave'),
tooltip=translate('leave'),
ng-if="!project.trashed"
aria-label=translate('trash'),
tooltip=translate('trash'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
ng-click="trash($event)"
)
i.icon.fa.fa-sign-out(aria-hidden="true")
i.icon.fa.fa-trash(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
ng-if="project.archived && !project.trashed"
aria-label=translate('unarchive'),
tooltip=translate('unarchive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="restore($event)"
ng-click="unarchive($event)"
)
i.icon.fa.fa-reply(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
aria-label=translate('delete_forever'),
tooltip=translate('delete_forever'),
ng-if="project.trashed && !project.archived"
aria-label=translate('untrash'),
tooltip=translate('untrash'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="deleteProject($event)"
ng-click="untrash($event)"
)
i.icon.fa.fa-trash(aria-hidden="true")
i.icon.fa.fa-reply(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.trashed && !project.archived && !isOwner()"
aria-label=translate('leave'),
tooltip=translate('leave'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="leave($event)"
)
i.icon.fa.fa-sign-out(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.trashed && !project.archived && isOwner()"
aria-label=translate('delete'),
tooltip=translate('delete'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="delete($event)"
)
i.icon.fa.fa-ban(aria-hidden="true")
div(
ng-if="project.isTableActionInflight"
aria-label=translate('processing')

View File

@@ -184,36 +184,54 @@ script(type='text/ng-template', id='newProjectModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")} ...
script(type='text/ng-template', id='deleteProjectsModalTemplate')
script(type='text/ng-template', id='archiveTrashLeaveOrDeleteProjectsModalTemplate')
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) ×
h3(ng-if="action == 'delete'") #{translate("delete_projects")}
h3(ng-if="action == 'archive'") #{translate("archive_projects")}
h3(ng-if="action == 'leave'") #{translate("leave_projects")}
h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")}
h3(ng-if="action == 'archive-and-leave'") #{translate("archive_and_leave_projects")}
h3(ng-if="action === 'archive'") #{translate("archive_projects")}
h3(ng-if="action === 'trash'") #{translate("trash_projects")}
h3(ng-if="action === 'leave'") #{translate("leave_projects")}
h3(ng-if="action === 'delete'") #{translate("delete_projects")}
h3(ng-if="action === 'leaveOrDelete'") #{translate("delete_and_leave_projects")}
.modal-body
div(ng-show="projectsToDelete.length > 0")
p(ng-if="action == 'delete' || action == 'delete-and-leave'") #{translate("about_to_delete_projects")}
p(ng-if="action == 'archive' || action == 'archive-and-leave'") #{translate("about_to_archive_projects")}
div(ng-if="action !== 'leaveOrDelete'")
p(ng-if="action === 'archive'") #{translate("about_to_archive_projects")}
p(ng-if="action === 'trash'") #{translate("about_to_trash_projects")}
p(ng-if="action === 'leave'") #{translate("about_to_leave_projects")}
p(ng-if="action === 'delete'") #{translate("about_to_delete_projects")}
ul
li(ng-repeat="project in projectsToDelete | orderBy:'name'")
li(ng-repeat="project in projects | orderBy:'name'")
strong {{project.name}}
.project-action-alert.alert.alert-info(ng-if="action === 'archive'")
.project-action-alert-msg #{translate("archived_projects_info_note")}
a(href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized").project-action-alert-btn.btn.btn-info.pull-right #{translate("find_out_more")}
.project-action-alert.alert.alert-info(ng-if="action === 'trash'")
.project-action-alert-msg #{translate("trashed_projects_info_note")}
a(href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized").project-action-alert-btn.btn.btn-info.pull-right #{translate("find_out_more")}
.project-action-alert.alert.alert-warning(ng-if="action === 'leave' || action === 'delete'")
i.fa.fa-fw.fa-exclamation-triangle
.project-action-alert-msg #{translate("this_action_cannot_be_undone")}
div(ng-if="action === 'leaveOrDelete'")
p #{translate("about_to_delete_projects")}
ul
li(ng-repeat="project in projects | filter:{accessLevel: 'owner'} | orderBy:'name'")
strong {{project.name}}
div(ng-show="projectsToLeave.length > 0")
p #{translate("about_to_leave_projects")}
ul
li(ng-repeat="project in projectsToLeave | orderBy:'name'")
li(ng-repeat="project in projects | filter:{accessLevel: '!owner'} | orderBy:'name'")
strong {{project.name}}
.project-action-alert.alert.alert-warning
i.fa.fa-fw.fa-exclamation-triangle
.project-action-alert-msg #{translate("this_action_cannot_be_undone")}
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-danger(
ng-click="delete()"
ng-click="confirm()"
) #{translate("confirm")}
script(type="text/template", id="qq-project-uploader-template")

View File

@@ -24,7 +24,7 @@
) #{translate('clear_search')}
.project-tools(ng-cloak)
.btn-toolbar(ng-show="filter != 'archived'")
.btn-toolbar
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href,
@@ -37,15 +37,28 @@
i.fa.fa-cloud-download(aria-hidden="true")
a.btn.btn-default(
href,
aria-label=`{{ isArchiveableProjectSelected ? '${translate("archive")}' : '${translate("leave")}' }}`,
tooltip=`{{ isArchiveableProjectSelected ? '${translate("archive")}' : '${translate("leave")}' }}`,
ng-if="filter !== 'archived'"
aria-label=translate("archive"),
tooltip=translate("archive"),
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
)
i.fa(ng-class=`isArchiveableProjectSelected ? 'fa-inbox' : 'fa-sign-out'` aria-hidden="true")
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
i.fa.fa-inbox(aria-hidden="true")
a.btn.btn-default(
href,
ng-if="filter !== 'trashed'"
aria-label=translate("trash"),
tooltip=translate("trash"),
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openTrashProjectsModal()"
)
i.fa.fa-trash(aria-hidden="true")
.btn-group.dropdown(
ng-hide="selectedProjects.length < 1 || filter === 'archived' || filter === 'trashed'",
dropdown
)
a.btn.btn-default.dropdown-toggle(
href,
data-toggle="dropdown",
@@ -54,7 +67,7 @@
tooltip-append-to-body="true",
tooltip-placement="bottom"
)
i.fa.fa-folder-open-o
i.fa.fa-folder-open
|
span.caret
span.sr-only #{translate('add_to_folders')}
@@ -82,7 +95,10 @@
li
a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
.btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
.btn-group.dropdown(
ng-hide="selectedProjects.length != 1 || filter === 'archived' || filter === 'trashed'",
dropdown
)
a.btn.btn-default.dropdown-toggle(
href,
data-toggle="dropdown",
@@ -101,24 +117,51 @@
ng-click="openCloneProjectModal()"
) #{translate("make_copy")}
.btn-toolbar(ng-show="filter == 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1")
.btn-group(ng-show="filter === 'archived' && selectedProjects.length > 0")
a.btn.btn-default(
href,
data-original-title="Restore",
data-original-title=translate("unarchive"),
data-toggle="tooltip",
data-placement="bottom",
ng-click="restoreSelectedProjects()"
) #{translate("restore")}
ng-click="unarchiveProjects(selectedProjects)"
) #{translate("unarchive")}
.btn-group(ng-hide="selectedProjects.length < 1")
.btn-group(ng-show="filter === 'trashed' && selectedProjects.length > 0")
a.btn.btn-default(
href,
data-original-title=translate("untrash"),
data-toggle="tooltip",
data-placement="bottom",
ng-click="untrashProjects(selectedProjects)"
) #{translate("untrash")}
.btn-group(ng-show="filter === 'trashed' && selectedProjects.length > 0")
a.btn.btn-danger(
href,
data-original-title="Delete Forever",
ng-if="hasLeavableProjectsSelected() && !hasDeletableProjectsSelected()",
data-original-title=translate('leave'),
data-toggle="tooltip",
data-placement="bottom",
ng-click="openLeaveProjectsModal()"
) #{translate("leave")}
a.btn.btn-danger(
href,
ng-if="hasDeletableProjectsSelected() && !hasLeavableProjectsSelected()",
data-original-title=translate('delete'),
data-toggle="tooltip",
data-placement="bottom",
ng-click="openDeleteProjectsModal()"
) #{translate("delete_forever")}
) #{translate("delete")}
a.btn.btn-danger(
href,
ng-if="hasDeletableProjectsSelected() && hasLeavableProjectsSelected()",
data-original-title=translate('delete_and_leave'),
data-toggle="tooltip",
data-placement="bottom",
ng-click="openLeaveOrDeleteProjectsModal()"
) #{translate("delete_and_leave")}
.row.row-spaced
each warning in warnings

View File

@@ -58,8 +58,24 @@
a(href) #{translate("your_projects")}
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
a(href) #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
li.folders-menu-item-with-tooltip(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
a(href) #{settings.overleaf ? translate("archived_projects") : translate("deleted_projects")}
.folders-menu-tooltip-trigger(
href
tooltip=translate('archived_projects_info_note')
tooltip-placement="right"
tooltip-append-to-body="true"
stop-propagation="click"
) i
li.folders-menu-item-with-tooltip(ng-class="{active: (filter == 'trashed')}", ng-click="filterProjects('trashed')")
a(href) #{translate("trashed_projects")}
.folders-menu-tooltip-trigger(
href
tooltip=translate('trashed_projects_info_note')
tooltip-placement="right"
tooltip-append-to-body="true"
stop-propagation="click"
) i
if isShowingV1Projects
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
a(href) #{translate("v1_projects")}

View File

@@ -135,53 +135,20 @@ define(['base'], function(App) {
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
})
App.controller('DeleteProjectsModalController', function(
App.controller('ArchiveTrashLeaveOrDeleteProjectsModalController', function(
$scope,
$modalInstance,
$timeout,
projects
projects,
action
) {
$scope.projectsToDelete = projects.filter(
project => project.accessLevel === 'owner'
)
$scope.projectsToLeave = projects.filter(
project => project.accessLevel !== 'owner'
)
$scope.projectsToArchive = projects.filter(
project => project.accessLevel === 'owner' && !project.archived
)
$scope.projects = projects
if (
$scope.projectsToLeave.length > 0 &&
$scope.projectsToDelete.length > 0
) {
if (
$scope.projectsToArchive.length > 0 &&
window.ExposedSettings.isOverleaf
) {
$scope.action = 'archive-and-leave'
} else {
$scope.action = 'delete-and-leave'
}
} else if (
$scope.projectsToLeave.length === 0 &&
$scope.projectsToDelete.length > 0
) {
if (
$scope.projectsToArchive.length > 0 &&
window.ExposedSettings.isOverleaf
) {
$scope.action = 'archive'
} else {
$scope.action = 'delete'
}
} else {
$scope.action = 'leave'
}
$scope.action = action
$scope.delete = () => $modalInstance.close()
$scope.confirm = () => $modalInstance.close({ projects, action })
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
$scope.cancel = () => $modalInstance.dismiss('cancel')
})
App.controller('UploadProjectModalController', function(

View File

@@ -15,7 +15,6 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
$scope.notificationsInstitution = window.data.notificationsInstitution
$scope.allSelected = false
$scope.selectedProjects = []
$scope.isArchiveableProjectSelected = false
$scope.filter = 'all'
$scope.predicate = 'lastUpdated'
$scope.nUntagged = 0
@@ -182,9 +181,6 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
$scope.selectedProjects = $scope.projects.filter(
project => project.selected
)
$scope.isArchiveableProjectSelected = $scope.selectedProjects.some(
project => window.user_id === project.owner._id
)
}
$scope.getSelectedProjects = () => $scope.selectedProjects
@@ -194,6 +190,18 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
$scope.getFirstSelectedProject = () => $scope.selectedProjects[0]
$scope.hasLeavableProjectsSelected = () =>
_.some(
$scope.getSelectedProjects(),
project => project.accessLevel !== 'owner' && project.trashed
)
$scope.hasDeletableProjectsSelected = () =>
_.some(
$scope.getSelectedProjects(),
project => project.accessLevel === 'owner' && project.trashed
)
$scope.updateVisibleProjects = function() {
$scope.visibleProjects = []
const selectedTag = $scope.getSelectedTag()
@@ -253,6 +261,18 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
}
}
if ($scope.filter === 'trashed') {
// Only show trashed projects
if (!project.trashed) {
visible = false
}
} else {
// Only show non-trashed projects
if (project.trashed) {
visible = false
}
}
if ($scope.filter === 'v1' && !project.isV1Project) {
visible = false
}
@@ -543,64 +563,243 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
})
}
$scope.createArchiveProjectsModal = function(projects) {
// Methods to create modals for archiving, trashing, leaving and deleting projects
const _createArchiveTrashLeaveOrDeleteProjectsModal = function(
action,
projects
) {
eventTracking.send(
'project-list-page-interaction',
'project action',
action
)
return $modal.open({
templateUrl: 'deleteProjectsModalTemplate',
controller: 'DeleteProjectsModalController',
templateUrl: 'archiveTrashLeaveOrDeleteProjectsModalTemplate',
controller: 'ArchiveTrashLeaveOrDeleteProjectsModalController',
resolve: {
projects() {
return projects
},
action() {
return action
}
}
})
}
$scope.createArchiveProjectsModal = function(projects) {
return _createArchiveTrashLeaveOrDeleteProjectsModal('archive', projects)
}
$scope.createTrashProjectsModal = function(projects) {
return _createArchiveTrashLeaveOrDeleteProjectsModal('trash', projects)
}
$scope.createLeaveProjectsModal = function(projects) {
return _createArchiveTrashLeaveOrDeleteProjectsModal('leave', projects)
}
$scope.createDeleteProjectsModal = function(projects) {
return _createArchiveTrashLeaveOrDeleteProjectsModal('delete', projects)
}
$scope.createLeaveOrDeleteProjectsModal = function(projects) {
return _createArchiveTrashLeaveOrDeleteProjectsModal(
'leaveOrDelete',
projects
)
}
//
$scope.openArchiveProjectsModal = function() {
const modalInstance = $scope.createArchiveProjectsModal(
$scope.getSelectedProjects()
)
eventTracking.send(
'project-list-page-interaction',
'project action',
'Delete'
)
modalInstance.result.then(() => $scope.archiveOrLeaveSelectedProjects())
modalInstance.result.then(() => $scope.archiveSelectedProjects())
}
$scope.archiveOrLeaveSelectedProjects = () =>
$scope.archiveOrLeaveProjects($scope.getSelectedProjects())
$scope.openTrashProjectsModal = function() {
const modalInstance = $scope.createTrashProjectsModal(
$scope.getSelectedProjects()
)
$scope.archiveOrLeaveProjects = function(projects) {
modalInstance.result.then(() => $scope.trashSelectedProjects())
}
$scope.openLeaveProjectsModal = function() {
const modalInstance = $scope.createLeaveProjectsModal(
$scope.getSelectedProjects()
)
modalInstance.result.then(() => $scope.leaveSelectedProjects())
}
$scope.openDeleteProjectsModal = function() {
const modalInstance = $scope.createDeleteProjectsModal(
$scope.getSelectedProjects()
)
modalInstance.result.then(() => $scope.deleteSelectedProjects())
}
$scope.openLeaveOrDeleteProjectsModal = function() {
const modalInstance = $scope.createLeaveOrDeleteProjectsModal(
$scope.getSelectedProjects()
)
modalInstance.result.then(() => $scope.leaveOrDeleteSelectedProjects())
}
//
$scope.archiveSelectedProjects = () =>
$scope.archiveProjects($scope.getSelectedProjects())
$scope.unarchiveSelectedProjects = () =>
$scope.unarchiveProjects($scope.getSelectedProjects())
$scope.trashSelectedProjects = () =>
$scope.trashProjects($scope.getSelectedProjects())
$scope.untrashSelectedProjects = () =>
$scope.untrashProjects($scope.getSelectedProjects())
$scope.leaveSelectedProjects = () =>
$scope.leaveProjects($scope.getSelectedProjects())
$scope.deleteSelectedProjects = () =>
$scope.deleteProjects($scope.getSelectedProjects())
$scope.leaveOrDeleteSelectedProjects = () =>
$scope.leaveOrDeleteProjects($scope.getSelectedProjects())
//
$scope.archiveProjects = function(projects) {
for (let project of projects) {
$scope.archiveOrLeaveProject(project)
project.archived = true
project.trashed = false
_archiveProject(project)
}
$scope.updateVisibleProjects()
}
$scope.archiveOrLeaveProject = function(project) {
if (project.accessLevel === 'owner') {
project.archived = true
queuedHttp({
method: 'DELETE',
url: `/project/${project.id}`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
} else {
$scope._removeProjectFromList(project)
$scope.unarchiveProjects = function(projects) {
for (let project of projects) {
project.archived = false
_unarchiveProject(project)
}
$scope.updateVisibleProjects()
}
for (let tag of project.tags || []) {
$scope._removeProjectIdsFromTagArray(tag, [project._id])
$scope.trashProjects = function(projects) {
for (let project of projects) {
project.trashed = true
project.archived = false
_trashProject(project)
}
$scope.updateVisibleProjects()
}
$scope.untrashProjects = function(projects) {
for (let project of projects) {
project.trashed = false
_untrashProject(project)
}
$scope.updateVisibleProjects()
}
$scope.leaveProjects = function(projects) {
_deleteOrLeaveProjectsLocally(projects)
for (let project of projects) {
_leaveProject(project)
}
$scope.updateVisibleProjects()
}
$scope.deleteProjects = function(projects) {
_deleteOrLeaveProjectsLocally(projects)
for (let project of projects) {
_deleteProject(project)
}
$scope.updateVisibleProjects()
}
$scope.leaveOrDeleteProjects = function(projects) {
_deleteOrLeaveProjectsLocally(projects)
for (let project of projects) {
if (project.accessLevel === 'owner') {
_deleteProject(project)
} else {
_leaveProject(project)
}
}
$scope.updateVisibleProjects()
}
queuedHttp({
method: 'POST',
url: `/project/${project.id}/leave`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
// Actual interaction with the backend---we could move this into a service
const _archiveProject = function(project) {
return queuedHttp({
method: 'POST',
url: `/project/${project.id}/archive`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _unarchiveProject = function(project) {
return queuedHttp({
method: 'DELETE',
url: `/project/${project.id}/archive`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _trashProject = function(project) {
return queuedHttp({
method: 'POST',
url: `/project/${project.id}/trash`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _untrashProject = function(project) {
return queuedHttp({
method: 'DELETE',
url: `/project/${project.id}/trash`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _leaveProject = function(project) {
return queuedHttp({
method: 'POST',
url: `/project/${project.id}/leave`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _deleteProject = function(project) {
return queuedHttp({
method: 'DELETE',
url: `/project/${project.id}?forever=true`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
const _deleteOrLeaveProjectsLocally = function(projects) {
const projectIds = projects.map(p => p.id)
for (let tag of $scope.tags || []) {
$scope._removeProjectIdsFromTagArray(tag, projectIds)
}
for (let project of projects || []) {
$scope._removeProjectFromList(project)
}
}
@@ -612,69 +811,6 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
}
}
$scope.openDeleteProjectsModal = function() {
const modalInstance = $modal.open({
templateUrl: 'deleteProjectsModalTemplate',
controller: 'DeleteProjectsModalController',
resolve: {
projects() {
return $scope.getSelectedProjects()
}
}
})
modalInstance.result.then(() => $scope.deleteSelectedProjects())
}
$scope.deleteSelectedProjects = function() {
const selectedProjects = $scope.getSelectedProjects()
const selectedProjectIds = $scope.getSelectedProjectIds()
// Remove projects from array
for (let project of selectedProjects) {
$scope._removeProjectFromList(project)
}
// Remove project from any tags
for (let tag of $scope.tags) {
$scope._removeProjectIdsFromTagArray(tag, selectedProjectIds)
}
for (let projectId of selectedProjectIds) {
queuedHttp({
method: 'DELETE',
url: `/project/${projectId}?forever=true`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
$scope.updateVisibleProjects()
}
$scope.restoreSelectedProjects = () =>
$scope.restoreProjects($scope.getSelectedProjects())
$scope.restoreProjects = function(projects) {
const projectIds = projects.map(p => p.id)
for (let project of projects) {
project.archived = false
}
for (let projectId of projectIds) {
queuedHttp({
method: 'POST',
url: `/project/${projectId}/restore`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
}
$scope.updateVisibleProjects()
}
$scope.openUploadProjectModal = function() {
$modal.open({
templateUrl: 'uploadProjectModalTemplate',
@@ -728,9 +864,6 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
queuedHttp,
ProjectListService
) {
$scope.shouldDisableCheckbox = project =>
$scope.filter === 'archived' && project.accessLevel !== 'owner'
$scope.projectLink = function(project) {
if (
project.accessLevel === 'readAndWrite' &&
@@ -796,49 +929,41 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
$scope.downloadProjectsById([$scope.project.id])
}
$scope.archiveOrLeave = function(e) {
$scope.archive = function(e) {
e.stopPropagation()
$scope.createArchiveProjectsModal([$scope.project]).result.then(() => {
$scope.archiveOrLeaveProject($scope.project)
$scope.updateVisibleProjects()
$scope.archiveProjects([$scope.project])
})
}
$scope.restore = function(e) {
$scope.unarchive = function(e) {
e.stopPropagation()
$scope.restoreProjects([$scope.project])
$scope.unarchiveProjects([$scope.project])
}
$scope.deleteProject = function(e) {
$scope.trash = function(e) {
e.stopPropagation()
const modalInstance = $modal.open({
templateUrl: 'deleteProjectsModalTemplate',
controller: 'DeleteProjectsModalController',
resolve: {
projects() {
return [$scope.project]
}
}
$scope.createTrashProjectsModal([$scope.project]).result.then(() => {
$scope.trashProjects([$scope.project])
})
}
modalInstance.result.then(function() {
$scope.project.isTableActionInflight = true
return queuedHttp({
method: 'DELETE',
url: `/project/${$scope.project.id}?forever=true`,
headers: {
'X-CSRF-Token': window.csrfToken
}
})
.then(function() {
$scope.project.isTableActionInflight = false
$scope._removeProjectFromList($scope.project)
for (let tag of $scope.tags) {
$scope._removeProjectIdsFromTagArray(tag, [$scope.project.id])
}
$scope.updateVisibleProjects()
})
.catch(() => ($scope.project.isTableActionInflight = false))
$scope.untrash = function(e) {
e.stopPropagation()
$scope.untrashProjects([$scope.project])
}
$scope.leave = function(e) {
e.stopPropagation()
$scope.createLeaveProjectsModal([$scope.project]).result.then(() => {
$scope.leaveProjects([$scope.project])
})
}
$scope.delete = function(e) {
e.stopPropagation()
$scope.createDeleteProjectsModal([$scope.project]).result.then(() => {
$scope.deleteProjects([$scope.project])
})
}
})

View File

@@ -350,6 +350,12 @@ ul.folders-menu {
}
}
}
> li.folders-menu-item-with-tooltip {
> a {
padding-right: 1.5 * @folders-menu-item-h-padding +
@folders-menu-tooltip-size;
}
}
> li > a.small {
color: @gray;
}
@@ -430,6 +436,37 @@ ul.folders-menu {
}
}
.folders-menu-tooltip-trigger {
display: none;
position: absolute;
top: 50%;
right: @folders-menu-item-h-padding;
width: @folders-menu-tooltip-size;
height: @folders-menu-tooltip-size;
margin-top: -(floor(@folders-menu-tooltip-size / 2));
line-height: @folders-menu-tooltip-size;
background-color: @folders-menu-tooltip-bg;
font-family: @font-family-serif;
color: #fff;
font-style: italic;
border-radius: ceil(@folders-menu-tooltip-size / 2);
text-align: center;
font-weight: 700;
@media (min-width: @screen-sm-min) {
display: block;
}
}
.project-action-alert {
display: flex;
margin-bottom: 0;
align-items: center;
}
.project-action-alert-msg {
flex-grow: 1;
padding-right: 10px;
}
form.project-search {
.form-group {
margin-bottom: 0;

View File

@@ -909,7 +909,8 @@
@folders-tag-border-color : @text-color;
@folders-tag-menu-active-hover : darken(@brand-primary, 10%);
@folders-tag-menu-hover : @gray-light;
@folders-menu-tooltip-size : 21px;
@folders-menu-tooltip-bg : @blue;
// Progress bars
@progress-border-radius : @border-radius-base;
@progress-border-width : 1px;

View File

@@ -1,6 +1,6 @@
@import "./_common-variables.less";
@import './_common-variables.less';
@font-family-sans-serif: "Lato", sans-serif;
@font-family-sans-serif: 'Lato', sans-serif;
@header-height: 68px;
@footer-height: 50px;
@@ -14,388 +14,388 @@
@ol-blue-gray-5 : #2C3645;
@ol-blue-gray-6 : #1E2530;
@ol-green : #138A07;
@ol-dark-green : #004A0E;
@ol-blue : #3E70BB;
@ol-dark-blue : #2857A1;
@ol-red : #C9453E;
@ol-dark-red : #A6312B;
@ol-green : #138A07;
@ol-dark-green : #004A0E;
@ol-blue : #3E70BB;
@ol-dark-blue : #2857A1;
@ol-red : #C9453E;
@ol-dark-red : #A6312B;
@ol-type-color : @ol-blue-gray-3;
@accent-color-primary: @ol-green;
@foo-color: @ol-blue;
@ol-type-color : @ol-blue-gray-3;
@accent-color-primary: @ol-green;
@accent-color-secondary: @ol-dark-green;
// Navbar customization
@navbar-title-color : @ol-blue-gray-1;
@navbar-title-color : @ol-blue-gray-1;
@navbar-title-color-hover : @ol-blue-gray-2;
@navbar-brand-width : 130px;
@navbar-default-color : #FFF;
@navbar-default-bg : @ol-blue-gray-6;
@navbar-default-border : transparent;
@navbar-brand-image-url : url(/img/ol-brand/overleaf-white.svg);
@navbar-default-link-bg : transparent;
@navbar-brand-width : 130px;
@navbar-default-color : #FFF;
@navbar-default-bg : @ol-blue-gray-6;
@navbar-default-border : transparent;
@navbar-brand-image-url : url(/img/ol-brand/overleaf-white.svg);
@navbar-default-link-bg : transparent;
@nav-pills-active-link-hover-bg: @ol-dark-green;
@nav-pills-link-color : @btn-default-bg;
@nav-pills-link-hover-bg : darken(@ol-blue-gray-4, 8%); // match button-variant mixin
@nav-pills-link-color : @btn-default-bg;
@nav-pills-link-hover-bg : darken(@ol-blue-gray-4, 8%); // match button-variant mixin
// Backgrounds
@body-bg : #FFF;
@content-alt-bg-color : @ol-blue-gray-0;
@body-bg : #FFF;
@content-alt-bg-color : @ol-blue-gray-0;
// Border
@border-color-base: @ol-blue-gray-2;
@border-color-base: @ol-blue-gray-2;
// Typography
@text-small-color : @ol-type-color;
@text-color : @ol-type-color;
@link-color : @ol-blue;
@link-color-alt : @ol-green;
@link-active-color : @ol-dark-green;
@link-hover-color : @ol-dark-blue;
@link-hover-color-alt : @ol-dark-green;
@hr-border : @ol-blue-gray-1;
@hr-border-alt : @gray-lighter;
@text-small-color : @ol-type-color;
@text-color : @ol-type-color;
@link-color : @ol-blue;
@link-color-alt : @ol-green;
@link-active-color : @ol-dark-green;
@link-hover-color : @ol-dark-blue;
@link-hover-color-alt : @ol-dark-green;
@hr-border : @ol-blue-gray-1;
@hr-border-alt : @gray-lighter;
@blockquote-small-color: @ol-blue-gray-3;
// Button colors and sizing
@btn-border-width : 0;
@btn-border-width : 0;
@btn-border-bottom-width : 0;
@btn-border-radius-large : 9999px;
@btn-border-radius-base : 9999px;
@btn-border-radius-base : 9999px;
@btn-border-radius-small : 9999px;
@btn-default-color : #FFF;
@btn-default-bg : @ol-blue-gray-4;
@btn-default-border : transparent;
@btn-default-color : #FFF;
@btn-default-bg : @ol-blue-gray-4;
@btn-default-border : transparent;
@btn-primary-color : #FFF;
@btn-primary-bg : @ol-green;
@btn-primary-border : transparent;
@btn-primary-color : #FFF;
@btn-primary-bg : @ol-green;
@btn-primary-border : transparent;
@btn-success-color : #FFF;
@btn-success-bg : @ol-green;
@btn-success-border : transparent;
@btn-success-color : #FFF;
@btn-success-bg : @ol-green;
@btn-success-border : transparent;
@btn-info-color : #FFF;
@btn-info-bg : @ol-blue;
@btn-info-border : transparent;
@btn-info-color : #FFF;
@btn-info-bg : @ol-blue;
@btn-info-border : transparent;
@btn-switch-color : @ol-blue-gray-4;
@btn-switch-hover-color : darken(@ol-blue-gray-4, 8%);
@btn-switch-color : @ol-blue-gray-4;
@btn-switch-hover-color : darken(@ol-blue-gray-4, 8%);
// Padding
@padding-xs-horizontal : 8px;
@padding-xs-horizontal : 8px;
// Alerts
@alert-padding : 15px;
@alert-border-radius : @border-radius-base;
@alert-link-font-weight : bold;
@alert-success-bg : @brand-success;
@alert-success-text : #FFF;
@alert-success-bg : @brand-success;
@alert-success-text : #FFF;
@alert-success-border: transparent;
@alert-info-bg : @brand-info;
@alert-info-text : #FFF;
@alert-info-border : transparent;
@alert-info-bg : @brand-info;
@alert-info-text : #FFF;
@alert-info-border : transparent;
@alert-warning-bg : @brand-warning;
@alert-warning-text : #FFF;
@alert-warning-bg : @brand-warning;
@alert-warning-text : #FFF;
@alert-warning-border: transparent;
@alert-danger-bg : @brand-danger;
@alert-danger-text : #FFF;
@alert-danger-bg : @brand-danger;
@alert-danger-text : #FFF;
@alert-danger-border : transparent;
@alert-alt-bg : @ol-blue-gray-1;
@alert-alt-text : @ol-type-color;
@alert-alt-bg : @ol-blue-gray-1;
@alert-alt-text : @ol-type-color;
@alert-alt-border: transparent;
// Tags
@tag-border-radius : 9999px;
@tag-color : @ol-blue-gray-4;
@tag-bg-color : @ol-blue-gray-1;
@tag-bg-hover-color : darken(@ol-blue-gray-1, 5%);
@tag-top-adjustment : 2px;
@labels-font-size : 85%;
@tag-border-radius : 9999px;
@tag-color : @ol-blue-gray-4;
@tag-bg-color : @ol-blue-gray-1;
@tag-bg-hover-color : darken(@ol-blue-gray-1, 5%);
@tag-top-adjustment : 2px;
@labels-font-size : 85%;
// Navbar
@grid-float-breakpoint : @screen-md-min;
@navbar-default-padding-v : (@grid-gutter-width / 2);
@navbar-default-padding-h : 10px;
@navbar-default-padding : @navbar-default-padding-v @navbar-default-padding-h;
@navbar-default-link-color : #FFF;
@grid-float-breakpoint : @screen-md-min;
@navbar-default-padding-v : (@grid-gutter-width / 2);
@navbar-default-padding-h : 10px;
@navbar-default-padding : @navbar-default-padding-v @navbar-default-padding-h;
@navbar-default-link-color : #FFF;
@navbar-default-link-border-color : @navbar-default-link-color;
@navbar-default-link-hover-bg : @ol-green;
@navbar-default-link-active-bg : @ol-green;
@navbar-default-link-hover-color : @ol-green;
@navbar-btn-font-size : @font-size-base;
@navbar-btn-border-radius : @btn-border-radius-base;
@navbar-btn-font-weight : 400;
@navbar-btn-padding : (@padding-base-vertical - 1) @padding-base-horizontal @padding-base-vertical;
@navbar-btn-line-height : @line-height-base;
@navbar-default-link-hover-bg : @ol-green;
@navbar-default-link-active-bg : @ol-green;
@navbar-default-link-hover-color : @ol-green;
@navbar-btn-font-size : @font-size-base;
@navbar-btn-border-radius : @btn-border-radius-base;
@navbar-btn-font-weight : 400;
@navbar-btn-padding : (@padding-base-vertical - 1) @padding-base-horizontal @padding-base-vertical;
@navbar-btn-line-height : @line-height-base;
@navbar-subdued-color : #FFF;
@navbar-subdued-padding : (@padding-base-vertical + 1) (@padding-base-horizontal + 1) (@padding-base-vertical + 2);
@navbar-subdued-hover-bg : #FFF;
@navbar-subdued-hover-color : @ol-green;
@navbar-subdued-color : #FFF;
@navbar-subdued-padding : (@padding-base-vertical + 1) (@padding-base-horizontal + 1) (@padding-base-vertical + 2);
@navbar-subdued-hover-bg : #FFF;
@navbar-subdued-hover-color : @ol-green;
@dropdown-divider-margin : 6px;
@dropdown-item-padding : 4px 20px;
@dropdown-divider-margin : 6px;
@dropdown-item-padding : 4px 20px;
// Forms
@input-color : @ol-blue-gray-3;
@input-border-radius : unit(@line-height-base, em);
@input-height-base : @line-height-computed + (@padding-base-vertical * 2) - 1;
@input-color : @ol-blue-gray-3;
@input-border-radius : unit(@line-height-base, em);
@input-height-base : @line-height-computed + (@padding-base-vertical * 2) - 1;
// TODO Warning color-orange?
@btn-warning-color : #FFF;
@btn-warning-bg : @ol-red;
@btn-warning-border : transparent;
@btn-warning-color : #FFF;
@btn-warning-bg : @ol-red;
@btn-warning-border : transparent;
@btn-danger-color : #FFF;
@btn-danger-bg : @ol-red;
@btn-danger-border : transparent;
@btn-danger-color : #FFF;
@btn-danger-bg : @ol-red;
@btn-danger-border : transparent;
// Cards
@card-box-shadow : none;
@card-box-shadow : none;
// Sidebar
@sidebar-bg : @ol-blue-gray-5;
@sidebar-color : @ol-blue-gray-2;
@sidebar-link-color : #FFF;
@sidebar-active-border-radius : 0;
@sidebar-active-bg : @ol-blue-gray-6;
@sidebar-active-color : #FFF;
@sidebar-active-font-weight : 700;
@sidebar-hover-bg : @ol-blue-gray-4;
@sidebar-hover-text-decoration : none;
@v2-dash-pane-bg : @ol-blue-gray-4;
@v2-dash-pane-link-color : #FFF;
@v2-dash-pane-color : #FFF;
@v2-dash-pane-toggle-color : #FFF;
@v2-dash-pane-btn-bg : @ol-blue-gray-5;
@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6;
@sidebar-bg : @ol-blue-gray-5;
@sidebar-color : @ol-blue-gray-2;
@sidebar-link-color : #FFF;
@sidebar-active-border-radius : 0;
@sidebar-active-bg : @ol-blue-gray-6;
@sidebar-active-color : #FFF;
@sidebar-active-font-weight : 700;
@sidebar-hover-bg : @ol-blue-gray-4;
@sidebar-hover-text-decoration : none;
@v2-dash-pane-bg : @ol-blue-gray-4;
@v2-dash-pane-link-color : #FFF;
@v2-dash-pane-color : #FFF;
@v2-dash-pane-toggle-color : #FFF;
@v2-dash-pane-btn-bg : @ol-blue-gray-5;
@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6;
@folders-menu-margin : 0 -(@grid-gutter-width / 2);
@folders-menu-line-height : @structured-list-line-height;
@folders-menu-item-v-padding : (@line-height-computed / 4);
@folders-menu-item-h-padding : (@grid-gutter-width / 2);
@folders-title-padding : @folders-menu-item-v-padding 0;
@folders-title-margin-top : 0;
@folders-title-margin-bottom : 0;
@folders-title-font-weight : normal;
@folders-title-font-size : @font-size-small;
@folders-title-color : @ol-blue-gray-2;
@folders-title-text-transform : uppercase;
@folders-tag-display : block;
@folders-tag-line-height : 1.4;
@folders-tag-padding : @folders-menu-item-v-padding 20px @folders-menu-item-v-padding @folders-menu-item-h-padding;
@folders-tag-menu-color : #FFF;
@folders-tag-hover : @sidebar-hover-bg;
@folders-tag-border-color : @folders-tag-menu-color;
@folders-tag-menu-hover : rgba(0, 0, 0, .1);
@folders-tag-menu-active-hover : rgba(0, 0, 0, .1);
@folders-menu-margin : 0 -(@grid-gutter-width / 2);
@folders-menu-line-height : @structured-list-line-height;
@folders-menu-item-v-padding : (@line-height-computed / 4);
@folders-menu-item-h-padding : (@grid-gutter-width / 2);
@folders-menu-tooltip-bg : @ol-blue;
@folders-title-padding : @folders-menu-item-v-padding 0;
@folders-title-margin-top : 0;
@folders-title-margin-bottom : 0;
@folders-title-font-weight : normal;
@folders-title-font-size : @font-size-small;
@folders-title-color : @ol-blue-gray-2;
@folders-title-text-transform : uppercase;
@folders-tag-display : block;
@folders-tag-line-height : 1.4;
@folders-tag-padding : @folders-menu-item-v-padding 20px @folders-menu-item-v-padding @folders-menu-item-h-padding;
@folders-tag-menu-color : #FFF;
@folders-tag-hover : @sidebar-hover-bg;
@folders-tag-border-color : @folders-tag-menu-color;
@folders-tag-menu-hover : rgba(0, 0, 0, .1);
@folders-tag-menu-active-hover : rgba(0, 0, 0, .1);
// Portal
@btn-portal-width : 200px;
@btn-portal-width : 200px;
// Project table
@structured-list-line-height : 2.5;
@structured-list-link-color : @ol-blue;
@structured-list-line-height : 2.5;
@structured-list-link-color : @ol-blue;
@structured-header-border-color : shade(@ol-blue-gray-1, 5%);
@structured-list-border-color : @ol-blue-gray-1;
@structured-list-hover-color : lighten(@ol-blue-gray-1, 5%);
@structured-list-border-color : @ol-blue-gray-1;
@structured-list-hover-color : lighten(@ol-blue-gray-1, 5%);
// Progress bars
@progress-border-radius : @line-height-computed;
@progress-border-width : 0;
@progress-bar-bg : @ol-blue-gray-4;
@progress-bar-success-bg : @ol-green;
@progress-bar-warning-bg : @brand-warning;
@progress-bar-danger-bg : @ol-red;
@progress-bar-info-bg : @ol-blue;
@progress-bar-shadow : none;
@progress-border-radius : @line-height-computed;
@progress-border-width : 0;
@progress-bar-bg : @ol-blue-gray-4;
@progress-bar-success-bg : @ol-green;
@progress-bar-warning-bg : @brand-warning;
@progress-bar-danger-bg : @ol-red;
@progress-bar-info-bg : @ol-blue;
@progress-bar-shadow : none;
// Footer
@footer-bg-color : #FFF;
@footer-link-color : @ol-green;
@footer-link-hover-color : @ol-dark-green;
@footer-padding : 2em 0;
@footer-bg-color : #FFF;
@footer-link-color : @ol-green;
@footer-link-hover-color : @ol-dark-green;
@footer-padding : 2em 0;
// Editor header
@toolbar-header-bg-color : @ol-blue-gray-6;
@toolbar-header-shadow : none;
@toolbar-header-bg-color : @ol-blue-gray-6;
@toolbar-header-shadow : none;
@toolbar-header-branded-btn-bg-color : transparent;
@toolbar-btn-color : #FFF;
@toolbar-btn-hover-color : #FFF;
@toolbar-btn-hover-bg-color : @ol-blue-gray-5;
@toolbar-btn-hover-text-shadow : none;
@toolbar-btn-active-color : #FFF;
@toolbar-btn-active-bg-color : @ol-green;
@toolbar-btn-active-shadow : none;
@toolbar-border-color : @ol-blue-gray-5;
@toolbar-header-btn-border-color : @toolbar-border-color;
@toolbar-alt-bg-color : @ol-blue-gray-5;
@toolbar-icon-btn-color : #FFF;
@toolbar-icon-btn-hover-color : #FFF;
@toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none;
@toolbar-font-size : 13px;
@project-name-color : @ol-blue-gray-2;
@project-rename-link-color : @ol-blue-gray-2;
@project-rename-link-color-hover : @ol-blue-gray-1;
@global-alerts-padding : 7px;
@toolbar-btn-color : #FFF;
@toolbar-btn-hover-color : #FFF;
@toolbar-btn-hover-bg-color : @ol-blue-gray-5;
@toolbar-btn-hover-text-shadow : none;
@toolbar-btn-active-color : #FFF;
@toolbar-btn-active-bg-color : @ol-green;
@toolbar-btn-active-shadow : none;
@toolbar-border-color : @ol-blue-gray-5;
@toolbar-header-btn-border-color : @toolbar-border-color;
@toolbar-alt-bg-color : @ol-blue-gray-5;
@toolbar-icon-btn-color : #FFF;
@toolbar-icon-btn-hover-color : #FFF;
@toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none;
@toolbar-font-size : 13px;
@project-name-color : @ol-blue-gray-2;
@project-rename-link-color : @ol-blue-gray-2;
@project-rename-link-color-hover : @ol-blue-gray-1;
@global-alerts-padding : 7px;
// Editor file-tree
@file-tree-bg : @ol-blue-gray-4;
@file-tree-line-height : 2.05;
@file-tree-item-color : #FFF;
@file-tree-item-focus-color : @file-tree-item-color;
@file-tree-bg : @ol-blue-gray-4;
@file-tree-line-height : 2.05;
@file-tree-item-color : #FFF;
@file-tree-item-focus-color : @file-tree-item-color;
@file-tree-item-focus-selected-color : @file-tree-item-color;
@file-tree-item-selected-color : @file-tree-item-color;
@file-tree-item-input-color : @ol-blue-gray-5;
@file-tree-item-toggle-color : @ol-blue-gray-2;
@file-tree-item-icon-color : @ol-blue-gray-2;
@file-tree-item-folder-color : @ol-blue-gray-2;
@file-tree-item-hover-bg : @ol-blue-gray-5;
@file-tree-item-selected-bg : @ol-green;
@file-tree-multiselect-bg : @ol-blue;
@file-tree-multiselect-hover-bg : @ol-dark-blue;
@file-tree-droppable-bg-color : @ol-blue-gray-2;
@file-tree-item-selected-color : @file-tree-item-color;
@file-tree-item-input-color : @ol-blue-gray-5;
@file-tree-item-toggle-color : @ol-blue-gray-2;
@file-tree-item-icon-color : @ol-blue-gray-2;
@file-tree-item-folder-color : @ol-blue-gray-2;
@file-tree-item-hover-bg : @ol-blue-gray-5;
@file-tree-item-selected-bg : @ol-green;
@file-tree-multiselect-bg : @ol-blue;
@file-tree-multiselect-hover-bg : @ol-dark-blue;
@file-tree-droppable-bg-color : @ol-blue-gray-2;
// Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-5;
@editor-resizer-bg-color : @ol-blue-gray-5;
@editor-resizer-bg-color-dragging : @ol-blue-gray-5;
@editor-toggler-bg-color : darken(@ol-blue-gray-2, 15%);
@editor-toggler-hover-bg-color : @ol-green;
@synctex-controls-z-index : 6;
@synctex-controls-padding : 0;
@editor-border-color : @ol-blue-gray-5;
@editor-toggler-bg-color : darken(@ol-blue-gray-2, 15%);
@editor-toggler-hover-bg-color : @ol-green;
@synctex-controls-z-index : 6;
@synctex-controls-padding : 0;
@editor-border-color : @ol-blue-gray-5;
// Editor toolbar
@editor-toolbar-bg : @ol-blue-gray-5;
@editor-toolbar-bg : @ol-blue-gray-5;
// Toggle switch
@toggle-switch-bg : @ol-blue-gray-1;
@toggle-switch-highlight-color : @ol-green;
@toggle-switch-radius-left : @btn-border-radius-base 0 0 @btn-border-radius-base;
@toggle-switch-radius-right : 0 @btn-border-radius-base @btn-border-radius-base 0;
@toggle-switch-bg : @ol-blue-gray-1;
@toggle-switch-highlight-color : @ol-green;
@toggle-switch-radius-left : @btn-border-radius-base 0 0 @btn-border-radius-base;
@toggle-switch-radius-right : 0 @btn-border-radius-base @btn-border-radius-base 0;
// Formatting buttons
@formatting-btn-color : #FFF;
@formatting-btn-bg : @ol-blue-gray-5;
@formatting-btn-border : @ol-blue-gray-4;
@formatting-menu-bg : @ol-blue-gray-5;
@formatting-btn-color : #FFF;
@formatting-btn-bg : @ol-blue-gray-5;
@formatting-btn-border : @ol-blue-gray-4;
@formatting-menu-bg : @ol-blue-gray-5;
// Chat
@chat-bg : @ol-blue-gray-5;
@chat-message-color : #FFF;
@chat-message-name-color : #FFF;
@chat-message-date-color : @ol-blue-gray-2;
@chat-message-box-shadow : none;
@chat-message-padding : 5px 10px;
@chat-message-border-radius : @border-radius-large;
@chat-message-weight : bold;
@chat-new-message-bg : @ol-blue-gray-4;
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
@chat-new-message-border-color : @editor-border-color;
@chat-bg : @ol-blue-gray-5;
@chat-message-color : #FFF;
@chat-message-name-color : #FFF;
@chat-message-date-color : @ol-blue-gray-2;
@chat-message-box-shadow : none;
@chat-message-padding : 5px 10px;
@chat-message-border-radius : @border-radius-large;
@chat-message-weight : bold;
@chat-new-message-bg : @ol-blue-gray-4;
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
@chat-new-message-border-color : @editor-border-color;
// Pagination
@pagination-active-bg : @ol-dark-green;
@pagination-active-border : @gray-lighter;
@pagination-active-color : #FFF;
@pagination-bg : #FFF;
@pagination-border : @gray-lighter;
@pagination-color : @ol-dark-green;
@pagination-disabled-color : @gray-dark;
@pagination-disabled-bg : @gray-lightest;
@pagination-disabled-border : @gray-lighter;
@pagination-hover-color : @ol-dark-green;
@pagination-hover-bg : @gray-lightest;
@pagination-hover-border : @gray-lighter;
@pagination-active-bg : @ol-dark-green;
@pagination-active-border : @gray-lighter;
@pagination-active-color : #FFF;
@pagination-bg : #FFF;
@pagination-border : @gray-lighter;
@pagination-color : @ol-dark-green;
@pagination-disabled-color : @gray-dark;
@pagination-disabled-bg : @gray-lightest;
@pagination-disabled-border : @gray-lighter;
@pagination-hover-color : @ol-dark-green;
@pagination-hover-bg : @gray-lightest;
@pagination-hover-border : @gray-lighter;
// PDF
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;
@pdfjs-bg : transparent;
@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;
@pdfjs-bg : transparent;
@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
// Plans
@table-hover-bg : @ol-blue-gray-0;
@plans-non-highlighted : white;
@table-hover-bg : @ol-blue-gray-0;
@plans-non-highlighted : white;
// Portals
@black-alpha-strong : rgba(0,0,0,0.8);
@black-alpha-strong : rgba(0,0,0,0.8);
// v2 History
@history-base-font-size : @font-size-small;
@history-base-bg : @ol-blue-gray-1;
@history-entry-label-bg-color : @ol-blue;
@history-entry-pseudo-label-bg-color : @ol-green;
@history-entry-label-color : #FFF;
@history-entry-selected-label-bg-color : #FFF;
@history-entry-selected-label-color : @ol-blue;
@history-base-font-size : @font-size-small;
@history-base-bg : @ol-blue-gray-1;
@history-entry-label-bg-color : @ol-blue;
@history-entry-pseudo-label-bg-color : @ol-green;
@history-entry-label-color : #FFF;
@history-entry-selected-label-bg-color : #FFF;
@history-entry-selected-label-color : @ol-blue;
@history-entry-selected-pseudo-label-color: @ol-green;
@history-entry-day-bg : @ol-blue-gray-2;
@history-entry-selected-bg : @ol-green;
@history-entry-handle-bg : darken(@ol-green, 10%);
@history-entry-handle-height : 8px;
@history-base-color : @ol-blue-gray-2;
@history-highlight-color : @ol-type-color;
@history-toolbar-bg-color : @editor-toolbar-bg;
@history-toolbar-color : #FFF;
@history-file-badge-bg : rgba(255, 255, 255, .25);
@history-file-badge-color : @file-tree-item-color;
@history-entry-day-bg : @ol-blue-gray-2;
@history-entry-selected-bg : @ol-green;
@history-entry-handle-bg : darken(@ol-green, 10%);
@history-entry-handle-height : 8px;
@history-base-color : @ol-blue-gray-2;
@history-highlight-color : @ol-type-color;
@history-toolbar-bg-color : @editor-toolbar-bg;
@history-toolbar-color : #FFF;
@history-file-badge-bg : rgba(255, 255, 255, .25);
@history-file-badge-color : @file-tree-item-color;
// Screens
// added -size to not conflict with common_variables
@screen-size-sm-max : 767px;
@screen-size-md-min : 768px;
@screen-size-md-max : 991px;
@screen-size-sm-max : 767px;
@screen-size-md-min : 768px;
@screen-size-md-max : 991px;
// System messages
@sys-msg-background : @ol-blue;
@sys-msg-color : #FFF;
@sys-msg-border : solid 1px lighten(@ol-blue, 10%);
@sys-msg-background : @ol-blue;
@sys-msg-color : #FFF;
@sys-msg-border : solid 1px lighten(@ol-blue, 10%);
@input-suggestion-v-offset : 4px;
@input-suggestion-v-offset : 4px;
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@gray-darker: #252525;
@gray-dark: #505050;
@gray: #7a7a7a;
@gray-light: #a4a4a4;
@gray-lighter: #cfcfcf;
@gray-lightest: #f0f0f0;
@white: #ffffff;
@gray-darker: #252525;
@gray-dark: #505050;
@gray: #7a7a7a;
@gray-light: #a4a4a4;
@gray-lighter: #cfcfcf;
@gray-lightest: #f0f0f0;
@white: #ffffff;
@blue: #405ebf;
@blueDark: #040D2D;
@green: #46a546;
@red: #a93529;
@yellow: #A1A729;
@orange: #f89406;
@pink: #c3325f;
@purple: #7a43b6;
@blue: #405ebf;
@blueDark: #040d2d;
@green: #46a546;
@red: #a93529;
@yellow: #a1a729;
@orange: #f89406;
@pink: #c3325f;
@purple: #7a43b6;
@brand-primary: @ol-green;
@brand-secondary: @ol-dark-green;
@brand-success: @green;
@brand-info: @ol-blue;
@brand-warning: @orange;
@brand-danger: @ol-red;
@brand-primary: @ol-green;
@brand-secondary: @ol-dark-green;
@brand-success: @green;
@brand-info: @ol-blue;
@brand-warning: @orange;
@brand-danger: @ol-red;
@editor-header-logo-background: url(/img/ol-brand/overleaf-o-white.svg) center / contain no-repeat;
@editor-header-logo-background: url(/img/ol-brand/overleaf-o-white.svg) center /
contain no-repeat;
@editor-loading-logo-padding-top: 115.44%;
@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);
@editor-loading-logo-foreground-url: url(/img/ol-brand/overleaf-o.svg);
@editor-loading-logo-foreground-url: url(/img/ol-brand/overleaf-o.svg);

View File

@@ -1223,6 +1223,8 @@ describe('ProjectController', function() {
describe('_buildProjectViewModel', function() {
beforeEach(function() {
this.ProjectHelper.isArchived.returns(false)
this.ProjectHelper.isTrashed.returns(false)
this.project = {
_id: 'abcd',
name: 'netsenits',
@@ -1239,31 +1241,70 @@ describe('ProjectController', function() {
}
})
it('should produce a model of the project', function() {
const result = this.ProjectController._buildProjectViewModel(
this.project,
'readAndWrite',
'owner',
this.user._id
)
expect(result).to.exist
expect(result).to.be.an('object')
expect(result).to.deep.equal({
id: 'abcd',
name: 'netsenits',
lastUpdated: 1,
lastUpdatedBy: 2,
publicAccessLevel: 'private',
accessLevel: 'readAndWrite',
source: 'owner',
archived: false,
owner_ref: 'defg',
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
describe('project not being archived or trashed', function() {
it('should produce a model of the project', function() {
const result = this.ProjectController._buildProjectViewModel(
this.project,
'readAndWrite',
'owner',
this.user._id
)
expect(result).to.exist
expect(result).to.be.an('object')
expect(result).to.deep.equal({
id: 'abcd',
name: 'netsenits',
lastUpdated: 1,
lastUpdatedBy: 2,
publicAccessLevel: 'private',
accessLevel: 'readAndWrite',
source: 'owner',
archived: false,
trashed: false,
owner_ref: 'defg',
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
})
})
})
describe('project being simultaneously archived and trashed', function() {
beforeEach(function() {
this.ProjectHelper.isArchived.returns(true)
this.ProjectHelper.isTrashed.returns(true)
})
it('should produce a model of the project', function() {
const result = this.ProjectController._buildProjectViewModel(
this.project,
'readAndWrite',
'owner',
this.user._id
)
expect(result).to.exist
expect(result).to.be.an('object')
expect(result).to.deep.equal({
id: 'abcd',
name: 'netsenits',
lastUpdated: 1,
lastUpdatedBy: 2,
publicAccessLevel: 'private',
accessLevel: 'readAndWrite',
source: 'owner',
archived: true,
trashed: false,
owner_ref: 'defg',
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
})
})
})
@@ -1286,6 +1327,7 @@ describe('ProjectController', function() {
accessLevel: 'readOnly',
source: 'token',
archived: false,
trashed: false,
owner_ref: null,
tokens: {
readAndWrite: '1abcd',

View File

@@ -158,7 +158,7 @@ describe('ProjectDeleter', function() {
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../Docstore/DocstoreManager': this.DocstoreManager,
'./ProjectDetailsHandler': this.ProjectDetailsHandler,
'../../infrastructure/mongojs': { db: this.db },
'../../infrastructure/mongojs': { db: this.db, ObjectId },
'logger-sharelatex': this.logger
},
globals: {
@@ -464,7 +464,13 @@ describe('ProjectDeleter', function() {
.resolves(this.project)
this.ProjectMock.expects('update')
.withArgs({ _id: this.project_id }, { $set: { archived: archived } })
.withArgs(
{ _id: this.project_id },
{
$set: { archived: archived },
$pull: { trashed: ObjectId(this.user._id) }
}
)
.resolves()
})
@@ -501,6 +507,57 @@ describe('ProjectDeleter', function() {
})
})
describe('trashProject', function() {
beforeEach(function() {
this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id })
.chain('exec')
.resolves(this.project)
this.ProjectMock.expects('update')
.withArgs(
{ _id: this.project_id },
{
$addToSet: { trashed: ObjectId(this.user._id) },
$pull: { archived: ObjectId(this.user._id) }
}
)
.resolves()
})
it('should update the project', async function() {
await this.ProjectDeleter.promises.trashProject(
this.project_id,
this.user._id
)
this.ProjectMock.verify()
})
})
describe('untrashProject', function() {
beforeEach(function() {
this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id })
.chain('exec')
.resolves(this.project)
this.ProjectMock.expects('update')
.withArgs(
{ _id: this.project_id },
{ $pull: { trashed: ObjectId(this.user._id) } }
)
.resolves()
})
it('should update the project', async function() {
await this.ProjectDeleter.promises.untrashProject(
this.project_id,
this.user._id
)
this.ProjectMock.verify()
})
})
describe('restoreProject', function() {
beforeEach(function() {
this.ProjectMock.expects('update')