project-list.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  1. define(['base', 'main/project-list/services/project-list'], function(App) {
  2. App.controller('ProjectPageController', function(
  3. $scope,
  4. $modal,
  5. $window,
  6. queuedHttp,
  7. eventTracking, // eslint-disable-line camelcase
  8. $timeout,
  9. localStorage,
  10. ProjectListService
  11. ) {
  12. $scope.projects = window.data.projects
  13. $scope.tags = window.data.tags
  14. $scope.notifications = window.data.notifications
  15. $scope.notificationsInstitution = window.data.notificationsInstitution
  16. $scope.allSelected = false
  17. $scope.selectedProjects = []
  18. $scope.filter = 'all'
  19. $scope.predicate = 'lastUpdated'
  20. $scope.nUntagged = 0
  21. $scope.reverse = true
  22. $scope.searchText = { value: '' }
  23. $scope.$watch('predicate', function(newValue) {
  24. $scope.comparator =
  25. newValue === 'ownerName' ? ownerNameComparator : defaultComparator
  26. })
  27. $timeout(() => recalculateProjectListHeight(), 10)
  28. $scope.$watch(
  29. () =>
  30. $scope.projects.filter(
  31. project =>
  32. (project.tags == null || project.tags.length === 0) &&
  33. !project.archived
  34. ).length,
  35. newVal => ($scope.nUntagged = newVal)
  36. )
  37. var recalculateProjectListHeight = function() {
  38. const $projListCard = $('.project-list-card')
  39. if (!$projListCard || !$projListCard.offset()) return
  40. const topOffset = $projListCard.offset().top
  41. const cardPadding = $projListCard.outerHeight() - $projListCard.height()
  42. const bottomOffset = $('footer').outerHeight()
  43. const height =
  44. $window.innerHeight - topOffset - bottomOffset - cardPadding
  45. $scope.projectListHeight = height
  46. }
  47. function defaultComparator(v1, v2) {
  48. var result = 0
  49. var type1 = v1.type
  50. var type2 = v2.type
  51. if ($scope.predicate === 'ownerName') {
  52. return
  53. }
  54. if (type1 === type2) {
  55. var value1 = v1.value
  56. var value2 = v2.value
  57. if (type1 === 'string') {
  58. // Compare strings case-insensitively
  59. value1 = value1.toLowerCase()
  60. value2 = value2.toLowerCase()
  61. } else if (type1 === 'object') {
  62. // For basic objects, use the position of the object
  63. // in the collection instead of the value
  64. if (angular.isObject(value1)) value1 = v1.index
  65. if (angular.isObject(value2)) value2 = v2.index
  66. }
  67. if (value1 !== value2) {
  68. result = value1 < value2 ? -1 : 1
  69. }
  70. } else {
  71. result = type1 < type2 ? -1 : 1
  72. }
  73. return result
  74. }
  75. function ownerNameComparator(v1, v2) {
  76. if ($scope.predicate !== 'ownerName') {
  77. return
  78. }
  79. if (v1.value === 'You') {
  80. if (v2.value === 'You') {
  81. return v1.index < v2.index ? -1 : 1
  82. } else {
  83. return 1
  84. }
  85. } else if (v1.value === 'An Overleaf v1 User' || v1.value === 'None') {
  86. if (v2.value === 'An Overleaf v1 User' || v2.value === 'None') {
  87. return v1.index < v2.index ? -1 : 1
  88. } else {
  89. return -1
  90. }
  91. } else {
  92. if (v2.value === 'You') {
  93. return -1
  94. } else if (v2.value === 'An Overleaf v1 User' || v2.value === 'None') {
  95. return 1
  96. } else {
  97. return v1.value > v2.value ? -1 : 1
  98. }
  99. }
  100. }
  101. angular.element($window).bind('resize', function() {
  102. recalculateProjectListHeight()
  103. $scope.$apply()
  104. })
  105. $scope.$on('project-list:notifications-received', () =>
  106. $scope.$applyAsync(() => recalculateProjectListHeight())
  107. )
  108. // Allow tags to be accessed on projects as well
  109. const projectsById = {}
  110. for (let project of $scope.projects) {
  111. projectsById[project.id] = project
  112. }
  113. $scope.getProjectById = id => projectsById[id]
  114. for (let tag of $scope.tags) {
  115. for (let projectId of tag.project_ids || []) {
  116. let project = projectsById[projectId]
  117. if (project) {
  118. if (!project.tags) {
  119. project.tags = []
  120. }
  121. project.tags.push(tag)
  122. }
  123. }
  124. }
  125. $scope.changePredicate = function(newPredicate) {
  126. if ($scope.predicate === newPredicate) {
  127. $scope.reverse = !$scope.reverse
  128. }
  129. $scope.predicate = newPredicate
  130. }
  131. $scope.getSortIconClass = function(column) {
  132. if (column === $scope.predicate && $scope.reverse) {
  133. return 'fa-caret-down'
  134. } else if (column === $scope.predicate && !$scope.reverse) {
  135. return 'fa-caret-up'
  136. } else {
  137. return ''
  138. }
  139. }
  140. $scope.searchProjects = function() {
  141. eventTracking.send(
  142. 'project-list-page-interaction',
  143. 'project-search',
  144. 'keydown'
  145. )
  146. $scope.updateVisibleProjects()
  147. }
  148. $scope.clearSearchText = function() {
  149. $scope.searchText.value = ''
  150. $scope.filter = 'all'
  151. $scope.$emit('search:clear')
  152. $scope.updateVisibleProjects()
  153. }
  154. $scope.setFilter = function(filter) {
  155. $scope.filter = filter
  156. $scope.updateVisibleProjects()
  157. }
  158. $scope.updateSelectedProjects = function() {
  159. $scope.selectedProjects = $scope.projects.filter(
  160. project => project.selected
  161. )
  162. }
  163. $scope.getSelectedProjects = () => $scope.selectedProjects
  164. $scope.getSelectedProjectIds = () =>
  165. $scope.selectedProjects.map(project => project.id)
  166. $scope.getFirstSelectedProject = () => $scope.selectedProjects[0]
  167. $scope.hasLeavableProjectsSelected = () =>
  168. _.some(
  169. $scope.getSelectedProjects(),
  170. project => project.accessLevel !== 'owner' && project.trashed
  171. )
  172. $scope.hasDeletableProjectsSelected = () =>
  173. _.some(
  174. $scope.getSelectedProjects(),
  175. project => project.accessLevel === 'owner' && project.trashed
  176. )
  177. $scope.updateVisibleProjects = function() {
  178. $scope.visibleProjects = []
  179. const selectedTag = $scope.getSelectedTag()
  180. for (let project of $scope.projects) {
  181. let visible = true
  182. // Only show if it matches any search text
  183. if ($scope.searchText.value !== '') {
  184. if (
  185. project.name
  186. .toLowerCase()
  187. .indexOf($scope.searchText.value.toLowerCase()) === -1
  188. ) {
  189. visible = false
  190. }
  191. }
  192. // Only show if it matches the selected tag
  193. if (
  194. $scope.filter === 'tag' &&
  195. selectedTag != null &&
  196. !selectedTag.project_ids.includes(project.id)
  197. ) {
  198. visible = false
  199. }
  200. // Hide tagged projects if we only want to see the uncategorized ones
  201. if (
  202. $scope.filter === 'untagged' &&
  203. (project.tags != null ? project.tags.length : undefined) > 0
  204. ) {
  205. visible = false
  206. }
  207. // Hide projects we own if we only want to see shared projects
  208. if ($scope.filter === 'shared' && project.accessLevel === 'owner') {
  209. visible = false
  210. }
  211. // Hide projects from V1 if we only want to see shared projects
  212. if ($scope.filter === 'shared' && project.isV1Project) {
  213. visible = false
  214. }
  215. // Hide projects we don't own if we only want to see owned projects
  216. if ($scope.filter === 'owned' && project.accessLevel !== 'owner') {
  217. visible = false
  218. }
  219. if ($scope.filter === 'archived') {
  220. // Only show archived projects
  221. if (!project.archived) {
  222. visible = false
  223. }
  224. } else {
  225. // Only show non-archived projects
  226. if (project.archived) {
  227. visible = false
  228. }
  229. }
  230. if ($scope.filter === 'trashed') {
  231. // Only show trashed projects
  232. if (!project.trashed) {
  233. visible = false
  234. }
  235. } else {
  236. // Only show non-trashed projects
  237. if (project.trashed) {
  238. visible = false
  239. }
  240. }
  241. if ($scope.filter === 'v1' && !project.isV1Project) {
  242. visible = false
  243. }
  244. if (visible) {
  245. $scope.visibleProjects.push(project)
  246. } else {
  247. // We don't want hidden selections
  248. project.selected = false
  249. }
  250. }
  251. localStorage(
  252. 'project_list',
  253. JSON.stringify({
  254. filter: $scope.filter,
  255. selectedTagId: selectedTag != null ? selectedTag._id : undefined
  256. })
  257. )
  258. $scope.updateSelectedProjects()
  259. }
  260. $scope.getSelectedTag = function() {
  261. for (let tag of $scope.tags) {
  262. if (tag.selected) {
  263. return tag
  264. }
  265. }
  266. return null
  267. }
  268. $scope._removeProjectIdsFromTagArray = function(tag, removeProjectIds) {
  269. // Remove project_id from tag.project_ids
  270. const remainingProjectIds = []
  271. const removedProjectIds = []
  272. for (let projectId of tag.project_ids) {
  273. if (!removeProjectIds.includes(projectId)) {
  274. remainingProjectIds.push(projectId)
  275. } else {
  276. removedProjectIds.push(projectId)
  277. }
  278. }
  279. tag.project_ids = remainingProjectIds
  280. return removedProjectIds
  281. }
  282. $scope._removeProjectFromList = function(project) {
  283. const index = $scope.projects.indexOf(project)
  284. if (index > -1) {
  285. $scope.projects.splice(index, 1)
  286. }
  287. }
  288. $scope.removeSelectedProjectsFromTag = function(tag) {
  289. tag.showWhenEmpty = true
  290. const selectedProjectIds = $scope.getSelectedProjectIds()
  291. const selectedProjects = $scope.getSelectedProjects()
  292. const removedProjectIds = $scope._removeProjectIdsFromTagArray(
  293. tag,
  294. selectedProjectIds
  295. )
  296. // Remove tag from project.tags
  297. for (let project of selectedProjects) {
  298. if (!project.tags) {
  299. project.tags = []
  300. }
  301. const index = project.tags.indexOf(tag)
  302. if (index > -1) {
  303. project.tags.splice(index, 1)
  304. }
  305. }
  306. for (let projectId of removedProjectIds) {
  307. queuedHttp({
  308. method: 'DELETE',
  309. url: `/tag/${tag._id}/project/${projectId}`,
  310. headers: {
  311. 'X-CSRF-Token': window.csrfToken
  312. }
  313. })
  314. }
  315. // If we're filtering by this tag then we need to remove
  316. // the projects from view
  317. $scope.updateVisibleProjects()
  318. }
  319. $scope.removeProjectFromTag = function(project, tag) {
  320. tag.showWhenEmpty = true
  321. if (!project.tags) {
  322. project.tags = []
  323. }
  324. const index = project.tags.indexOf(tag)
  325. if (index > -1) {
  326. $scope._removeProjectIdsFromTagArray(tag, [project.id])
  327. project.tags.splice(index, 1)
  328. queuedHttp({
  329. method: 'DELETE',
  330. url: `/tag/${tag._id}/project/${project.id}`,
  331. headers: {
  332. 'X-CSRF-Token': window.csrfToken
  333. }
  334. })
  335. $scope.updateVisibleProjects()
  336. }
  337. }
  338. $scope.addSelectedProjectsToTag = function(tag) {
  339. const selectedProjects = $scope.getSelectedProjects()
  340. eventTracking.send(
  341. 'project-list-page-interaction',
  342. 'project action',
  343. 'addSelectedProjectsToTag'
  344. )
  345. // Add project_ids into tag.project_ids
  346. const addedProjectIds = []
  347. for (let projectId of $scope.getSelectedProjectIds()) {
  348. if (!tag.project_ids.includes(projectId)) {
  349. tag.project_ids.push(projectId)
  350. addedProjectIds.push(projectId)
  351. }
  352. }
  353. // Add tag into each project.tags
  354. for (let project of selectedProjects) {
  355. if (!project.tags) {
  356. project.tags = []
  357. }
  358. if (!project.tags.includes(tag)) {
  359. project.tags.push(tag)
  360. }
  361. }
  362. for (let projectId of addedProjectIds) {
  363. queuedHttp.post(`/tag/${tag._id}/project/${projectId}`, {
  364. _csrf: window.csrfToken
  365. })
  366. }
  367. }
  368. $scope.openNewTagModal = function(e) {
  369. const modalInstance = $modal.open({
  370. templateUrl: 'newTagModalTemplate',
  371. controller: 'NewTagModalController'
  372. })
  373. modalInstance.result.then(function(tag) {
  374. const tagIsDuplicate = $scope.tags.find(function(existingTag) {
  375. return tag.name === existingTag.name
  376. })
  377. if (!tagIsDuplicate) {
  378. $scope.tags.push(tag)
  379. $scope.addSelectedProjectsToTag(tag)
  380. }
  381. })
  382. }
  383. $scope.createProject = function(name, template) {
  384. if (template == null) {
  385. template = 'none'
  386. }
  387. return queuedHttp
  388. .post('/project/new', {
  389. _csrf: window.csrfToken,
  390. projectName: name,
  391. template
  392. })
  393. .then(function(response) {
  394. const { data } = response
  395. $scope.projects.push({
  396. name,
  397. _id: data.project_id,
  398. accessLevel: 'owner',
  399. owner: {
  400. _id: window.user_id
  401. }
  402. // TODO: Check access level if correct after adding it in
  403. // to the rest of the app
  404. })
  405. $scope.updateVisibleProjects()
  406. })
  407. }
  408. $scope.openCreateProjectModal = function(template) {
  409. if (template == null) {
  410. template = 'none'
  411. }
  412. eventTracking.send(
  413. 'project-list-page-interaction',
  414. 'new-project',
  415. template
  416. )
  417. const modalInstance = $modal.open({
  418. templateUrl: 'newProjectModalTemplate',
  419. controller: 'NewProjectModalController',
  420. resolve: {
  421. template() {
  422. return template
  423. }
  424. },
  425. scope: $scope
  426. })
  427. modalInstance.result.then(
  428. projectId => (window.location = `/project/${projectId}`)
  429. )
  430. }
  431. $scope.renameProject = (project, newName) =>
  432. queuedHttp
  433. .post(`/project/${project.id}/rename`, {
  434. newProjectName: newName,
  435. _csrf: window.csrfToken
  436. })
  437. .then(() => (project.name = newName))
  438. $scope.openRenameProjectModal = function() {
  439. let project = $scope.getFirstSelectedProject()
  440. if (!project || project.accessLevel !== 'owner') {
  441. return
  442. }
  443. eventTracking.send(
  444. 'project-list-page-interaction',
  445. 'project action',
  446. 'Rename'
  447. )
  448. $modal.open({
  449. templateUrl: 'renameProjectModalTemplate',
  450. controller: 'RenameProjectModalController',
  451. resolve: {
  452. project() {
  453. return project
  454. }
  455. },
  456. scope: $scope
  457. })
  458. }
  459. $scope.cloneProject = function(project, cloneName) {
  460. eventTracking.send(
  461. 'project-list-page-interaction',
  462. 'project action',
  463. 'Clone'
  464. )
  465. return queuedHttp
  466. .post(`/project/${project.id}/clone`, {
  467. _csrf: window.csrfToken,
  468. projectName: cloneName
  469. })
  470. .then(function(response) {
  471. const { data } = response
  472. $scope.projects.push({
  473. name: data.name,
  474. id: data.project_id,
  475. accessLevel: 'owner',
  476. owner: {
  477. _id: data.owner_ref
  478. }
  479. // TODO: Check access level if correct after adding it in
  480. // to the rest of the app
  481. })
  482. $scope.updateVisibleProjects()
  483. })
  484. }
  485. $scope.openCloneProjectModal = function() {
  486. let project = $scope.getFirstSelectedProject()
  487. if (!project) {
  488. return
  489. }
  490. $modal.open({
  491. templateUrl: 'cloneProjectModalTemplate',
  492. controller: 'CloneProjectModalController',
  493. resolve: {
  494. project() {
  495. return project
  496. }
  497. },
  498. scope: $scope
  499. })
  500. }
  501. // Methods to create modals for archiving, trashing, leaving and deleting projects
  502. const _createArchiveTrashLeaveOrDeleteProjectsModal = function(
  503. action,
  504. projects
  505. ) {
  506. eventTracking.send(
  507. 'project-list-page-interaction',
  508. 'project action',
  509. action
  510. )
  511. return $modal.open({
  512. templateUrl: 'archiveTrashLeaveOrDeleteProjectsModalTemplate',
  513. controller: 'ArchiveTrashLeaveOrDeleteProjectsModalController',
  514. resolve: {
  515. projects() {
  516. return projects
  517. },
  518. action() {
  519. return action
  520. }
  521. }
  522. })
  523. }
  524. $scope.createArchiveProjectsModal = function(projects) {
  525. return _createArchiveTrashLeaveOrDeleteProjectsModal('archive', projects)
  526. }
  527. $scope.createTrashProjectsModal = function(projects) {
  528. return _createArchiveTrashLeaveOrDeleteProjectsModal('trash', projects)
  529. }
  530. $scope.createLeaveProjectsModal = function(projects) {
  531. return _createArchiveTrashLeaveOrDeleteProjectsModal('leave', projects)
  532. }
  533. $scope.createDeleteProjectsModal = function(projects) {
  534. return _createArchiveTrashLeaveOrDeleteProjectsModal('delete', projects)
  535. }
  536. $scope.createLeaveOrDeleteProjectsModal = function(projects) {
  537. return _createArchiveTrashLeaveOrDeleteProjectsModal(
  538. 'leaveOrDelete',
  539. projects
  540. )
  541. }
  542. //
  543. $scope.openArchiveProjectsModal = function() {
  544. const modalInstance = $scope.createArchiveProjectsModal(
  545. $scope.getSelectedProjects()
  546. )
  547. modalInstance.result.then(() => $scope.archiveSelectedProjects())
  548. }
  549. $scope.openTrashProjectsModal = function() {
  550. const modalInstance = $scope.createTrashProjectsModal(
  551. $scope.getSelectedProjects()
  552. )
  553. modalInstance.result.then(() => $scope.trashSelectedProjects())
  554. }
  555. $scope.openLeaveProjectsModal = function() {
  556. const modalInstance = $scope.createLeaveProjectsModal(
  557. $scope.getSelectedProjects()
  558. )
  559. modalInstance.result.then(() => $scope.leaveSelectedProjects())
  560. }
  561. $scope.openDeleteProjectsModal = function() {
  562. const modalInstance = $scope.createDeleteProjectsModal(
  563. $scope.getSelectedProjects()
  564. )
  565. modalInstance.result.then(() => $scope.deleteSelectedProjects())
  566. }
  567. $scope.openLeaveOrDeleteProjectsModal = function() {
  568. const modalInstance = $scope.createLeaveOrDeleteProjectsModal(
  569. $scope.getSelectedProjects()
  570. )
  571. modalInstance.result.then(() => $scope.leaveOrDeleteSelectedProjects())
  572. }
  573. //
  574. $scope.archiveSelectedProjects = () =>
  575. $scope.archiveProjects($scope.getSelectedProjects())
  576. $scope.unarchiveSelectedProjects = () =>
  577. $scope.unarchiveProjects($scope.getSelectedProjects())
  578. $scope.trashSelectedProjects = () =>
  579. $scope.trashProjects($scope.getSelectedProjects())
  580. $scope.untrashSelectedProjects = () =>
  581. $scope.untrashProjects($scope.getSelectedProjects())
  582. $scope.leaveSelectedProjects = () =>
  583. $scope.leaveProjects($scope.getSelectedProjects())
  584. $scope.deleteSelectedProjects = () =>
  585. $scope.deleteProjects($scope.getSelectedProjects())
  586. $scope.leaveOrDeleteSelectedProjects = () =>
  587. $scope.leaveOrDeleteProjects($scope.getSelectedProjects())
  588. //
  589. $scope.archiveProjects = function(projects) {
  590. for (let project of projects) {
  591. project.archived = true
  592. project.trashed = false
  593. _archiveProject(project)
  594. }
  595. $scope.updateVisibleProjects()
  596. }
  597. $scope.unarchiveProjects = function(projects) {
  598. for (let project of projects) {
  599. project.archived = false
  600. _unarchiveProject(project)
  601. }
  602. $scope.updateVisibleProjects()
  603. }
  604. $scope.trashProjects = function(projects) {
  605. for (let project of projects) {
  606. project.trashed = true
  607. project.archived = false
  608. _trashProject(project)
  609. }
  610. $scope.updateVisibleProjects()
  611. }
  612. $scope.untrashProjects = function(projects) {
  613. for (let project of projects) {
  614. project.trashed = false
  615. _untrashProject(project)
  616. }
  617. $scope.updateVisibleProjects()
  618. }
  619. $scope.leaveProjects = function(projects) {
  620. _deleteOrLeaveProjectsLocally(projects)
  621. for (let project of projects) {
  622. _leaveProject(project)
  623. }
  624. $scope.updateVisibleProjects()
  625. }
  626. $scope.deleteProjects = function(projects) {
  627. _deleteOrLeaveProjectsLocally(projects)
  628. for (let project of projects) {
  629. _deleteProject(project)
  630. }
  631. $scope.updateVisibleProjects()
  632. }
  633. $scope.leaveOrDeleteProjects = function(projects) {
  634. _deleteOrLeaveProjectsLocally(projects)
  635. for (let project of projects) {
  636. if (project.accessLevel === 'owner') {
  637. _deleteProject(project)
  638. } else {
  639. _leaveProject(project)
  640. }
  641. }
  642. $scope.updateVisibleProjects()
  643. }
  644. // Actual interaction with the backend---we could move this into a service
  645. const _archiveProject = function(project) {
  646. return queuedHttp({
  647. method: 'POST',
  648. url: `/project/${project.id}/archive`,
  649. headers: {
  650. 'X-CSRF-Token': window.csrfToken
  651. }
  652. })
  653. }
  654. const _unarchiveProject = function(project) {
  655. return queuedHttp({
  656. method: 'DELETE',
  657. url: `/project/${project.id}/archive`,
  658. headers: {
  659. 'X-CSRF-Token': window.csrfToken
  660. }
  661. })
  662. }
  663. const _trashProject = function(project) {
  664. return queuedHttp({
  665. method: 'POST',
  666. url: `/project/${project.id}/trash`,
  667. headers: {
  668. 'X-CSRF-Token': window.csrfToken
  669. }
  670. })
  671. }
  672. const _untrashProject = function(project) {
  673. return queuedHttp({
  674. method: 'DELETE',
  675. url: `/project/${project.id}/trash`,
  676. headers: {
  677. 'X-CSRF-Token': window.csrfToken
  678. }
  679. })
  680. }
  681. const _leaveProject = function(project) {
  682. return queuedHttp({
  683. method: 'POST',
  684. url: `/project/${project.id}/leave`,
  685. headers: {
  686. 'X-CSRF-Token': window.csrfToken
  687. }
  688. })
  689. }
  690. const _deleteProject = function(project) {
  691. return queuedHttp({
  692. method: 'DELETE',
  693. url: `/project/${project.id}`,
  694. headers: {
  695. 'X-CSRF-Token': window.csrfToken
  696. }
  697. })
  698. }
  699. const _deleteOrLeaveProjectsLocally = function(projects) {
  700. const projectIds = projects.map(p => p.id)
  701. for (let tag of $scope.tags || []) {
  702. $scope._removeProjectIdsFromTagArray(tag, projectIds)
  703. }
  704. for (let project of projects || []) {
  705. $scope._removeProjectFromList(project)
  706. }
  707. }
  708. $scope.getValueForCurrentPredicate = function(project) {
  709. if ($scope.predicate === 'ownerName') {
  710. return ProjectListService.getOwnerName(project)
  711. } else {
  712. return project[$scope.predicate]
  713. }
  714. }
  715. $scope.openUploadProjectModal = function() {
  716. $modal.open({
  717. templateUrl: 'uploadProjectModalTemplate',
  718. controller: 'UploadProjectModalController'
  719. })
  720. }
  721. $scope.downloadSelectedProjects = () =>
  722. $scope.downloadProjectsById($scope.getSelectedProjectIds())
  723. $scope.downloadProjectsById = function(projectIds) {
  724. let path
  725. eventTracking.send(
  726. 'project-list-page-interaction',
  727. 'project action',
  728. 'Download Zip'
  729. )
  730. if (projectIds.length > 1) {
  731. path = `/project/download/zip?project_ids=${projectIds.join(',')}`
  732. } else {
  733. path = `/project/${projectIds[0]}/download/zip`
  734. }
  735. return (window.location = path)
  736. }
  737. const markTagAsSelected = id => {
  738. for (let tag of $scope.tags) {
  739. if (tag._id === id) {
  740. tag.selected = true
  741. } else {
  742. tag.selected = false
  743. }
  744. }
  745. }
  746. const storedUIOpts = JSON.parse(localStorage('project_list'))
  747. if (storedUIOpts && storedUIOpts.filter) {
  748. if (storedUIOpts.filter === 'tag' && storedUIOpts.selectedTagId) {
  749. markTagAsSelected(storedUIOpts.selectedTagId)
  750. }
  751. $scope.setFilter(storedUIOpts.filter)
  752. } else {
  753. $scope.updateVisibleProjects()
  754. }
  755. })
  756. App.controller('ProjectListItemController', function(
  757. $scope,
  758. $modal,
  759. queuedHttp,
  760. ProjectListService
  761. ) {
  762. $scope.projectLink = function(project) {
  763. return `/project/${project.id}`
  764. }
  765. $scope.isLinkSharingProject = project => project.source === 'token'
  766. $scope.hasGenericOwnerName = () => {
  767. /* eslint-disable camelcase */
  768. const { first_name, last_name, email } = $scope.project.owner
  769. return !first_name && !last_name && !email
  770. /* eslint-enable camelcase */
  771. }
  772. $scope.getOwnerName = ProjectListService.getOwnerName
  773. $scope.getUserName = ProjectListService.getUserName
  774. $scope.isOwner = () =>
  775. $scope.project.owner && window.user_id === $scope.project.owner._id
  776. $scope.$watch('project.selected', function(value) {
  777. if (value != null) {
  778. $scope.updateSelectedProjects()
  779. }
  780. })
  781. $scope.clone = function(e) {
  782. e.stopPropagation()
  783. $scope.project.isTableActionInflight = true
  784. return $scope
  785. .cloneProject($scope.project, `${$scope.project.name} (Copy)`)
  786. .then(() => ($scope.project.isTableActionInflight = false))
  787. .catch(function(response) {
  788. const { data, status } = response
  789. const error = status === 400 ? { message: data } : true
  790. $modal.open({
  791. templateUrl: 'showErrorModalTemplate',
  792. controller: 'ShowErrorModalController',
  793. resolve: {
  794. error() {
  795. return error
  796. }
  797. }
  798. })
  799. $scope.project.isTableActionInflight = false
  800. })
  801. }
  802. $scope.download = function(e) {
  803. e.stopPropagation()
  804. $scope.downloadProjectsById([$scope.project.id])
  805. }
  806. $scope.archive = function(e) {
  807. e.stopPropagation()
  808. $scope.createArchiveProjectsModal([$scope.project]).result.then(() => {
  809. $scope.archiveProjects([$scope.project])
  810. })
  811. }
  812. $scope.unarchive = function(e) {
  813. e.stopPropagation()
  814. $scope.unarchiveProjects([$scope.project])
  815. }
  816. $scope.trash = function(e) {
  817. e.stopPropagation()
  818. $scope.createTrashProjectsModal([$scope.project]).result.then(() => {
  819. $scope.trashProjects([$scope.project])
  820. })
  821. }
  822. $scope.untrash = function(e) {
  823. e.stopPropagation()
  824. $scope.untrashProjects([$scope.project])
  825. }
  826. $scope.leave = function(e) {
  827. e.stopPropagation()
  828. $scope.createLeaveProjectsModal([$scope.project]).result.then(() => {
  829. $scope.leaveProjects([$scope.project])
  830. })
  831. }
  832. $scope.delete = function(e) {
  833. e.stopPropagation()
  834. $scope.createDeleteProjectsModal([$scope.project]).result.then(() => {
  835. $scope.deleteProjects([$scope.project])
  836. })
  837. }
  838. })
  839. })