project-list.js 25 KB

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