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.

524 lines
23 KiB

  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}}
  9. .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}}
  10. v-spacer
  11. v-btn.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
  12. v-icon mdi-refresh
  13. v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
  14. v-icon(left) mdi-check
  15. span {{$t('common:actions.apply')}}
  16. v-container.pa-0.mt-3(fluid, grid-list-lg)
  17. v-row(dense)
  18. v-col(cols='3')
  19. v-card.animated.fadeInUp
  20. v-toolbar(color='teal', dark, dense, flat, height='56')
  21. v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}}
  22. v-list(nav, two-line)
  23. v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
  24. v-list-item(value='TREE')
  25. v-list-item-avatar
  26. img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
  27. v-list-item-content
  28. v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}
  29. v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}
  30. v-list-item-avatar
  31. v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `TREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
  32. v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle
  33. v-list-item(value='MIXED')
  34. v-list-item-avatar
  35. img(src='/_assets/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
  36. v-list-item-content
  37. v-list-item-title {{$t('admin:navigation.modeCustom.title')}}
  38. v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}
  39. v-list-item-avatar
  40. v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `MIXED` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
  41. v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle
  42. v-list-item(value='STATIC')
  43. v-list-item-avatar
  44. img(src='/_assets/svg/icon-features-list.svg', alt='Static Navigation')
  45. v-list-item-content
  46. v-list-item-title {{$t('admin:navigation.modeStatic.title')}}
  47. v-list-item-subtitle {{$t('admin:navigation.modeStatic.description')}}
  48. v-list-item-avatar
  49. v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `STATIC` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
  50. v-icon(v-else, :color='config.mode === `STATIC` ? `teal` : `grey lighten-3`') mdi-check-circle
  51. v-list-item(value='NONE')
  52. v-list-item-avatar
  53. img(src='/_assets/svg/icon-cancel-dotted.svg', alt='None')
  54. v-list-item-content
  55. v-list-item-title {{$t('admin:navigation.modeNone.title')}}
  56. v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}
  57. v-list-item-avatar
  58. v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
  59. v-icon(v-else, :color='config.mode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle
  60. v-col(cols='9', v-if='config.mode === `MIXED` || config.mode === `STATIC`')
  61. v-card.animated.fadeInUp.wait-p2s
  62. v-row(no-gutters, align='stretch')
  63. v-col(style='flex: 0 0 350px;')
  64. v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`')
  65. .teal.lighten-1.pa-2.d-flex(style='margin-bottom: 1px; height:56px;')
  66. v-select(
  67. :disabled='locales.length < 2'
  68. label='Locale'
  69. hide-details
  70. solo
  71. flat
  72. background-color='teal darken-2'
  73. dark
  74. dense
  75. v-model='currentLang'
  76. :items='locales'
  77. item-text='nativeName'
  78. item-value='code'
  79. )
  80. v-tooltip(top)
  81. template(v-slot:activator='{ on }')
  82. v-btn.ml-2(icon, tile, color='white', v-on='on', @click='copyFromLocaleDialogIsShown = true')
  83. v-icon mdi-arrange-send-backward
  84. span {{$t('admin:navigation.copyFromLocale')}}
  85. v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;')
  86. v-list-item(v-if='currentTree.length < 1')
  87. v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert
  88. v-list-item-content
  89. em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}}
  90. draggable(v-model='currentTree')
  91. template(v-for='navItem in currentTree')
  92. v-list-item(
  93. v-if='navItem.kind === "link"'
  94. :key='navItem.id'
  95. :class='(navItem === current) ? "blue" : ""'
  96. @click='selectItem(navItem)'
  97. )
  98. v-list-item-avatar(size='24', tile)
  99. v-icon(v-if='navItem.icon.match(/fa[a-z] fa-/)', size='19') {{ navItem.icon }}
  100. v-icon(v-else) {{ navItem.icon }}
  101. v-list-item-title {{navItem.label}}
  102. .py-2.clickable(
  103. v-else-if='navItem.kind === "divider"'
  104. :key='navItem.id'
  105. :class='(navItem === current) ? "blue" : ""'
  106. @click='selectItem(navItem)'
  107. )
  108. v-divider
  109. v-subheader.pl-4.clickable(
  110. v-else-if='navItem.kind === "header"'
  111. :key='navItem.id'
  112. :class='(navItem === current) ? "blue" : ""'
  113. @click='selectItem(navItem)'
  114. ) {{navItem.label}}
  115. v-card-chin
  116. v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')
  117. template(v-slot:activator='{ on }')
  118. v-btn(v-on='on', color='primary', depressed, block)
  119. v-icon(left) mdi-plus
  120. span {{$t('common:actions.add')}}
  121. v-list
  122. v-list-item(@click='addItem("link")')
  123. v-list-item-avatar(size='24'): v-icon mdi-link
  124. v-list-item-title {{$t('navigation.link')}}
  125. v-list-item(@click='addItem("header")')
  126. v-list-item-avatar(size='24'): v-icon mdi-format-title
  127. v-list-item-title {{$t('navigation.header')}}
  128. v-list-item(@click='addItem("divider")')
  129. v-list-item-avatar(size='24'): v-icon mdi-minus
  130. v-list-item-title {{$t('navigation.divider')}}
  131. v-col
  132. v-card(flat, style='border-radius: 0 4px 4px 0;')
  133. template(v-if='current.kind === "link"')
  134. v-toolbar(height='56', color='teal lighten-1', flat, dark)
  135. .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}
  136. v-spacer
  137. v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
  138. v-icon(left) mdi-delete
  139. span {{$t('navigation.delete', { kind: $t('navigation.link') })}}
  140. v-card-text
  141. v-text-field(
  142. outlined
  143. :label='$t("navigation.label")'
  144. prepend-icon='mdi-format-title'
  145. v-model='current.label'
  146. counter='255'
  147. )
  148. v-text-field(
  149. outlined
  150. :label='$t("navigation.icon")'
  151. prepend-icon='mdi-dice-5'
  152. v-model='current.icon'
  153. hide-details
  154. )
  155. .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.
  156. .caption.pt-3.pl-5: strong Material Design Icons
  157. .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]
  158. .caption.pt-3.pl-5: strong Font Awesome 5
  159. .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).
  160. .caption.pt-3.pl-5: strong Font Awesome 4
  161. .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]
  162. v-divider
  163. v-card-text
  164. v-select(
  165. outlined
  166. :label='$t("navigation.targetType")'
  167. prepend-icon='mdi-near-me'
  168. :items='navTypes'
  169. v-model='current.targetType'
  170. hide-details
  171. )
  172. v-text-field.mt-4(
  173. v-if='current.targetType === `external` || current.targetType === `externalblank`'
  174. outlined
  175. :label='$t("navigation.target")'
  176. prepend-icon='mdi-near-me'
  177. v-model='current.target'
  178. hide-details
  179. )
  180. .d-flex.align-center.mt-4(v-else-if='current.targetType === "page"')
  181. v-btn.ml-8(
  182. color='primary'
  183. dark
  184. @click='selectPage'
  185. )
  186. v-icon(left) mdi-magnify
  187. span {{$t('admin:navigation.selectPageButton')}}
  188. .caption.ml-4.primary--text {{current.target}}
  189. v-text-field(
  190. v-else-if='current.targetType === `search`'
  191. outlined
  192. :label='$t("navigation.navType.searchQuery")'
  193. prepend-icon='search'
  194. v-model='current.target'
  195. )
  196. v-divider
  197. template(v-else-if='current.kind === "header"')
  198. v-toolbar(height='56', color='teal lighten-1', flat, dark)
  199. .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}
  200. v-spacer
  201. v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
  202. v-icon(left) mdi-delete
  203. span {{$t('navigation.delete', { kind: $t('navigation.header') })}}
  204. v-card-text
  205. v-text-field(
  206. outlined
  207. :label='$t("navigation.label")'
  208. prepend-icon='mdi-format-title'
  209. v-model='current.label'
  210. )
  211. v-divider
  212. div(v-else-if='current.kind === "divider"')
  213. v-toolbar(height='56', color='teal lighten-1', flat, dark)
  214. .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}}
  215. v-spacer
  216. v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
  217. v-icon(left) mdi-delete
  218. span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}
  219. v-card-text(v-if='current.kind')
  220. v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details)
  221. v-radio(:label='$t("admin:navigation.visibilityMode.all")', value='all', color='primary')
  222. v-radio.mt-3(:label='$t("admin:navigation.visibilityMode.restricted")', value='restricted', color='primary')
  223. .pl-8
  224. v-select.pl-8.mt-3(
  225. item-text='name'
  226. item-value='id'
  227. outlined
  228. prepend-icon='mdi-account-group'
  229. label='Groups'
  230. :disabled='current.visibilityMode !== `restricted`'
  231. v-model='current.visibilityGroups'
  232. :items='groups'
  233. persistent-hint
  234. clearable
  235. multiple
  236. )
  237. template(v-else)
  238. v-toolbar(height='56', color='teal lighten-1', flat, dark)
  239. v-card-text.grey--text(v-if='currentTree.length > 0') {{$t('navigation.noSelectionText')}}
  240. v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}
  241. v-dialog(v-model='copyFromLocaleDialogIsShown', max-width='650', persistent)
  242. v-card
  243. .dialog-header.is-short.is-teal
  244. v-icon.mr-3(color='white') mdi-arrange-send-backward
  245. span {{$t('admin:navigation.copyFromLocale')}}
  246. v-card-text.pt-5
  247. .body-2 {{$t('admin:navigation.copyFromLocaleInfoText')}}
  248. v-select.mt-3(
  249. :items='locales'
  250. item-text='nativeName'
  251. item-value='code'
  252. outlined
  253. prepend-icon='mdi-web'
  254. v-model='copyFromLocaleCode'
  255. :label='$t(`admin:navigation.sourceLocale`)'
  256. :hint='$t(`admin:navigation.sourceLocaleHint`)'
  257. persistent-hint
  258. )
  259. v-card-chin
  260. v-spacer
  261. v-btn(text, @click='copyFromLocaleDialogIsShown = false') {{$t('common:actions.cancel')}}
  262. v-btn.px-3(depressed, color='primary', @click='copyFromLocale')
  263. v-icon(left) mdi-chevron-right
  264. span {{$t('common:actions.copy')}}
  265. page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
  266. </template>
  267. <script>
  268. import _ from 'lodash'
  269. import gql from 'graphql-tag'
  270. import { v4 as uuid } from 'uuid'
  271. import groupsQuery from 'gql/admin/users/users-query-groups.gql'
  272. import draggable from 'vuedraggable'
  273. /* global siteConfig, siteLangs */
  274. export default {
  275. components: {
  276. draggable
  277. },
  278. data() {
  279. return {
  280. selectPageModal: false,
  281. trees: [],
  282. current: {},
  283. currentLang: siteConfig.lang,
  284. groups: [],
  285. copyFromLocaleDialogIsShown: false,
  286. config: {
  287. mode: 'NONE'
  288. },
  289. allLocales: [],
  290. copyFromLocaleCode: 'en'
  291. }
  292. },
  293. computed: {
  294. navTypes () {
  295. return [
  296. { text: this.$t('navigation.navType.external'), value: 'external' },
  297. { text: this.$t('navigation.navType.externalblank'), value: 'externalblank' },
  298. { text: this.$t('navigation.navType.home'), value: 'home' },
  299. { text: this.$t('navigation.navType.page'), value: 'page' }
  300. // { text: this.$t('navigation.navType.searchQuery'), value: 'search' }
  301. ]
  302. },
  303. locales () {
  304. return _.intersectionBy(this.allLocales, _.unionBy(siteLangs, [{ code: 'en' }, { code: siteConfig.lang }], 'code'), 'code')
  305. },
  306. currentTree: {
  307. get () {
  308. return _.get(_.find(this.trees, ['locale', this.currentLang]), 'items', null) || []
  309. },
  310. set (val) {
  311. const tree = _.find(this.trees, ['locale', this.currentLang])
  312. if (tree) {
  313. tree.items = val
  314. } else {
  315. this.trees = [...this.trees, {
  316. locale: this.currentLang,
  317. items: val
  318. }]
  319. }
  320. }
  321. }
  322. },
  323. watch: {
  324. currentLang (newValue, oldValue) {
  325. this.$nextTick(() => {
  326. if (this.currentTree.length > 0) {
  327. this.current = this.currentTree[0]
  328. } else {
  329. this.current = {}
  330. }
  331. })
  332. }
  333. },
  334. methods: {
  335. addItem(kind) {
  336. let newItem = {
  337. id: uuid(),
  338. kind,
  339. visibilityMode: 'all',
  340. visibilityGroups: []
  341. }
  342. switch (kind) {
  343. case 'link':
  344. newItem = {
  345. ...newItem,
  346. label: this.$t('navigation.untitled', { kind: this.$t(`navigation.link`) }),
  347. icon: 'mdi-chevron-right',
  348. targetType: 'home',
  349. target: ''
  350. }
  351. break
  352. case 'header':
  353. newItem.label = this.$t('navigation.untitled', { kind: this.$t(`navigation.header`) })
  354. break
  355. }
  356. this.currentTree = [...this.currentTree, newItem]
  357. this.current = newItem
  358. },
  359. deleteItem(item) {
  360. this.currentTree = _.pull(this.currentTree, item)
  361. this.current = {}
  362. },
  363. selectItem(item) {
  364. this.current = item
  365. },
  366. selectPage() {
  367. this.selectPageModal = true
  368. },
  369. selectPageHandle ({ path, locale }) {
  370. this.current.target = `/${locale}/${path}`
  371. },
  372. copyFromLocale () {
  373. this.copyFromLocaleDialogIsShown = false
  374. this.currentTree = [...this.currentTree, ..._.get(_.find(this.trees, ['locale', this.copyFromLocaleCode]), 'items', null) || []]
  375. },
  376. async save() {
  377. this.$store.commit(`loadingStart`, 'admin-navigation-save')
  378. try {
  379. const resp = await this.$apollo.mutate({
  380. mutation: gql`
  381. mutation ($tree: [NavigationTreeInput]!, $mode: NavigationMode!) {
  382. navigation{
  383. updateTree(tree: $tree) {
  384. responseResult {
  385. succeeded
  386. errorCode
  387. slug
  388. message
  389. }
  390. },
  391. updateConfig(mode: $mode) {
  392. responseResult {
  393. succeeded
  394. errorCode
  395. slug
  396. message
  397. }
  398. }
  399. }
  400. }
  401. `,
  402. variables: {
  403. tree: this.trees,
  404. mode: this.config.mode
  405. }
  406. })
  407. if (_.get(resp, 'data.navigation.updateTree.responseResult.succeeded', false) && _.get(resp, 'data.navigation.updateConfig.responseResult.succeeded', false)) {
  408. this.$store.commit('showNotification', {
  409. message: this.$t('navigation.saveSuccess'),
  410. style: 'success',
  411. icon: 'check'
  412. })
  413. } else {
  414. throw new Error(_.get(resp, 'data.navigation.updateTree.responseResult.message', 'An unexpected error occured.'))
  415. }
  416. } catch (err) {
  417. this.$store.commit('pushGraphError', err)
  418. }
  419. this.$store.commit(`loadingStop`, 'admin-navigation-save')
  420. },
  421. async refresh() {
  422. await this.$apollo.queries.trees.refetch()
  423. this.current = {}
  424. this.$store.commit('showNotification', {
  425. message: 'Navigation has been refreshed.',
  426. style: 'success',
  427. icon: 'cached'
  428. })
  429. }
  430. },
  431. apollo: {
  432. config: {
  433. query: gql`
  434. {
  435. navigation {
  436. config {
  437. mode
  438. }
  439. }
  440. }
  441. `,
  442. fetchPolicy: 'network-only',
  443. update: (data) => _.cloneDeep(data.navigation.config),
  444. watchLoading (isLoading) {
  445. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-config')
  446. }
  447. },
  448. trees: {
  449. query: gql`
  450. {
  451. navigation {
  452. tree {
  453. locale
  454. items {
  455. id
  456. kind
  457. label
  458. icon
  459. targetType
  460. target
  461. visibilityMode
  462. visibilityGroups
  463. }
  464. }
  465. }
  466. }
  467. `,
  468. fetchPolicy: 'network-only',
  469. update: (data) => _.cloneDeep(data.navigation.tree),
  470. watchLoading (isLoading) {
  471. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-tree')
  472. }
  473. },
  474. groups: {
  475. query: groupsQuery,
  476. fetchPolicy: 'network-only',
  477. update: (data) => data.groups.list,
  478. watchLoading (isLoading) {
  479. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-groups')
  480. }
  481. },
  482. allLocales: {
  483. query: gql`
  484. {
  485. localization {
  486. locales {
  487. code
  488. name
  489. nativeName
  490. }
  491. }
  492. }
  493. `,
  494. fetchPolicy: 'network-only',
  495. update: (data) => data.localization.locales,
  496. watchLoading (isLoading) {
  497. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-locales')
  498. }
  499. }
  500. }
  501. }
  502. </script>
  503. <style lang='scss' scoped>
  504. .clickable {
  505. cursor: pointer;
  506. &:hover {
  507. background-color: rgba(mc('blue', '500'), .25);
  508. }
  509. }
  510. </style>