You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Navigation.vue 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <!--
  2. - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
  3. -
  4. - @author Gary Kim <gary@garykim.dev>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <NcAppNavigation data-cy-files-navigation>
  24. <template #list>
  25. <NcAppNavigationItem v-for="view in parentViews"
  26. :key="view.id"
  27. :allow-collapse="true"
  28. :data-cy-files-navigation-item="view.id"
  29. :icon="view.iconClass"
  30. :open="view.expanded"
  31. :pinned="view.sticky"
  32. :title="view.name"
  33. :to="generateToNavigation(view)"
  34. @update:open="onToggleExpand(view)">
  35. <!-- Sanitized icon as svg if provided -->
  36. <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
  37. <!-- Child views if any -->
  38. <NcAppNavigationItem v-for="child in childViews[view.id]"
  39. :key="child.id"
  40. :data-cy-files-navigation-item="child.id"
  41. :exact="true"
  42. :icon="child.iconClass"
  43. :title="child.name"
  44. :to="generateToNavigation(child)">
  45. <!-- Sanitized icon as svg if provided -->
  46. <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
  47. </NcAppNavigationItem>
  48. </NcAppNavigationItem>
  49. </template>
  50. <!-- Non-scrollable navigation bottom elements -->
  51. <template #footer>
  52. <ul class="app-navigation-entry__settings">
  53. <!-- User storage usage statistics -->
  54. <NavigationQuota />
  55. <!-- Files settings modal toggle-->
  56. <NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')"
  57. :title="t('files', 'Files settings')"
  58. data-cy-files-navigation-settings-button
  59. @click.prevent.stop="openSettings">
  60. <Cog slot="icon" :size="20" />
  61. </NcAppNavigationItem>
  62. </ul>
  63. </template>
  64. <!-- Settings modal-->
  65. <SettingsModal :open="settingsOpened"
  66. data-cy-files-navigation-settings
  67. @close="onSettingsClose" />
  68. </NcAppNavigation>
  69. </template>
  70. <script>
  71. import { emit, subscribe } from '@nextcloud/event-bus'
  72. import { generateUrl } from '@nextcloud/router'
  73. import { translate } from '@nextcloud/l10n'
  74. import axios from '@nextcloud/axios'
  75. import Cog from 'vue-material-design-icons/Cog.vue'
  76. import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
  77. import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
  78. import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
  79. import logger from '../logger.js'
  80. import Navigation from '../services/Navigation.ts'
  81. import NavigationQuota from '../components/NavigationQuota.vue'
  82. import SettingsModal from './Settings.vue'
  83. import { setPageHeading } from '../../../../core/src/OCP/accessibility.js'
  84. export default {
  85. name: 'Navigation',
  86. components: {
  87. Cog,
  88. NavigationQuota,
  89. NcAppNavigation,
  90. NcAppNavigationItem,
  91. NcIconSvgWrapper,
  92. SettingsModal,
  93. },
  94. props: {
  95. // eslint-disable-next-line vue/prop-name-casing
  96. Navigation: {
  97. type: Navigation,
  98. required: true,
  99. },
  100. },
  101. data() {
  102. return {
  103. settingsOpened: false,
  104. }
  105. },
  106. computed: {
  107. currentViewId() {
  108. return this.$route?.params?.view || 'files'
  109. },
  110. /** @return {Navigation} */
  111. currentView() {
  112. return this.views.find(view => view.id === this.currentViewId)
  113. },
  114. /** @return {Navigation[]} */
  115. views() {
  116. return this.Navigation.views
  117. },
  118. /** @return {Navigation[]} */
  119. parentViews() {
  120. return this.views
  121. // filter child views
  122. .filter(view => !view.parent)
  123. // sort views by order
  124. .sort((a, b) => {
  125. return a.order - b.order
  126. })
  127. },
  128. /** @return {Navigation[]} */
  129. childViews() {
  130. return this.views
  131. // filter parent views
  132. .filter(view => !!view.parent)
  133. // create a map of parents and their children
  134. .reduce((list, view) => {
  135. list[view.parent] = [...(list[view.parent] || []), view]
  136. // Sort children by order
  137. list[view.parent].sort((a, b) => {
  138. return a.order - b.order
  139. })
  140. return list
  141. }, {})
  142. },
  143. },
  144. watch: {
  145. currentView(view, oldView) {
  146. // If undefined, it means we're initializing the view
  147. // This is handled by the legacy-view:initialized event
  148. if (view?.id === oldView?.id) {
  149. return
  150. }
  151. this.Navigation.setActive(view.id)
  152. logger.debug('Navigation changed', { id: view.id, view })
  153. // debugger
  154. this.showView(view, oldView)
  155. },
  156. },
  157. beforeMount() {
  158. if (this.currentView) {
  159. logger.debug('Navigation mounted. Showing requested view', { view: this.currentView })
  160. this.showView(this.currentView)
  161. }
  162. subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged)
  163. // TODO: remove this once the legacy navigation is gone
  164. subscribe('files:legacy-view:initialized', () => {
  165. logger.debug('Legacy view initialized', { ...this.currentView })
  166. this.showView(this.currentView)
  167. })
  168. },
  169. methods: {
  170. /**
  171. * @param {Navigation} view the new active view
  172. * @param {Navigation} oldView the old active view
  173. */
  174. showView(view, oldView) {
  175. // Closing any opened sidebar
  176. window?.OCA?.Files?.Sidebar?.close?.()
  177. if (view?.legacy) {
  178. const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer')
  179. document.querySelectorAll('#app-content .viewcontainer').forEach(el => {
  180. el.classList.add('hidden')
  181. })
  182. newAppContent.classList.remove('hidden')
  183. // Triggering legacy navigation events
  184. const { dir = '/' } = OC.Util.History.parseUrlQuery()
  185. const params = { itemId: view.id, dir }
  186. logger.debug('Triggering legacy navigation event', params)
  187. window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params))
  188. window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params))
  189. }
  190. this.Navigation.setActive(view)
  191. setPageHeading(view.name)
  192. emit('files:navigation:changed', view)
  193. },
  194. /**
  195. * Coming from the legacy files app.
  196. * TODO: remove when all views are migrated.
  197. *
  198. * @param {Navigation} view the new active view
  199. */
  200. onLegacyNavigationChanged({ id } = { id: 'files' }) {
  201. const view = this.Navigation.views.find(view => view.id === id)
  202. if (view && view.legacy && view.id !== this.currentView.id) {
  203. // Force update the current route as the request comes
  204. // from the legacy files app router
  205. this.$router.replace({ ...this.$route, params: { view: view.id } })
  206. this.Navigation.setActive(view)
  207. this.showView(view)
  208. }
  209. },
  210. /**
  211. * Expand/collapse a a view with children and permanently
  212. * save this setting in the server.
  213. *
  214. * @param {Navigation} view the view to toggle
  215. */
  216. onToggleExpand(view) {
  217. // Invert state
  218. view.expanded = !view.expanded
  219. axios.post(generateUrl(`/apps/files/api/v1/toggleShowFolder/${view.id}`), { show: view.expanded })
  220. },
  221. /**
  222. * Generate the route to a view
  223. *
  224. * @param {Navigation} view the view to toggle
  225. */
  226. generateToNavigation(view) {
  227. if (view.params) {
  228. const { dir, fileid } = view.params
  229. return { name: 'filelist', params: view.params, query: { dir, fileid } }
  230. }
  231. return { name: 'filelist', params: { view: view.id } }
  232. },
  233. /**
  234. * Open the settings modal
  235. */
  236. openSettings() {
  237. this.settingsOpened = true
  238. },
  239. /**
  240. * Close the settings modal
  241. */
  242. onSettingsClose() {
  243. this.settingsOpened = false
  244. },
  245. t: translate,
  246. },
  247. }
  248. </script>
  249. <style scoped lang="scss">
  250. // TODO: remove when https://github.com/nextcloud/nextcloud-vue/pull/3539 is in
  251. .app-navigation::v-deep .app-navigation-entry-icon {
  252. background-repeat: no-repeat;
  253. background-position: center;
  254. }
  255. .app-navigation > ul.app-navigation__list {
  256. // Use flex gap value for more elegant spacing
  257. padding-bottom: var(--default-grid-baseline, 4px);
  258. }
  259. .app-navigation-entry__settings {
  260. height: auto !important;
  261. overflow: hidden !important;
  262. padding-top: 0 !important;
  263. // Prevent shrinking or growing
  264. flex: 0 0 auto;
  265. }
  266. </style>