project-list.js 24 KB

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