public_html/src/layouts/resources/views/KnowledgeBase/KnowledgeBase.vue
<!--
/**
* KnowledgeBase component
*
* @description Knowledge base view root component
* @license YetiForce Public License 6.5
* @author Tomasz Poradzewski <t.poradzewski@yetiforce.com>
*/
-->
<template>
<div class="KnowledgeBase h-100">
<q-layout class="absolute" :style="coordinates.height && !maximized ? { 'max-height': `${coordinates.height - 31.14}px` } : {}" view="hHh Lpr fFf" container>
<q-header class="bg-white text-primary" elevated>
<q-toolbar class="flex-md-nowrap flex-wrap items-center q-gutter-x-md q-gutter-y-sm q-pl-md q-pr-none q-py-xs">
<div :class="['flex items-center no-wrap flex-md-grow-1 q-mr-sm-sm', searchData ? 'invisible' : '']">
<q-btn dense round push icon="mdi-menu" @click="toggleDrawer()">
<q-tooltip>{{ translate('JS_KB_TOGGLE_CATEGORY_MENU') }}</q-tooltip>
</q-btn>
<q-breadcrumbs class="ml-2" v-show="tab === 'categories'">
<template #separator>
<q-icon size="1.5em" name="mdi-chevron-right" />
</template>
<q-breadcrumbs-el
:class="[activeCategory === '' ? 'text-black' : 'cursor-pointer']"
:icon="tree.topCategory.icon"
:label="translate(tree.topCategory.label)"
@click="activeCategory === '' ? '' : fetchData()"
/>
<template v-if="activeCategory !== ''">
<q-breadcrumbs-el
v-for="(category, index) in tree.categories[activeCategory].parentTree"
:key="index"
:class="[index === tree.categories[activeCategory].parentTree.length - 1 ? 'text-black' : 'cursor-pointer']"
@click="index === tree.categories[activeCategory].parentTree.length - 1 ? '' : fetchData(category)"
>
<YfIcon v-if="tree.categories[category].icon" class="q-mr-sm" :size="iconSize" :icon="tree.categories[category].icon"></YfIcon>
{{ tree.categories[category].label }}
</q-breadcrumbs-el>
</template>
</q-breadcrumbs>
<q-breadcrumbs class="ml-2" v-show="tab === 'accounts'">
<q-breadcrumbs-el v-if="activeAccount !== ''" class="text-black">
<YfIcon :size="iconSize" :icon="'yfm-Accounts'" class="q-mr-sm"></YfIcon>
{{ activeAccount }}
</q-breadcrumbs-el>
</q-breadcrumbs>
</div>
<div class="tree-search flex flex-grow-1 no-wrap order-sm-none order-xs-last">
<q-input
v-model="filter"
class="full-width"
:placeholder="translate('JS_KB_SEARCH_PLACEHOLDER')"
rounded
outlined
type="search"
autofocus
@input="search"
>
<template #prepend>
<q-icon name="mdi-magnify" />
<q-tooltip v-model="inputFocus" anchor="top middle" self="center middle">{{
translate('JS_INPUT_TOO_SHORT').replace('_LENGTH_', '3')
}}</q-tooltip>
</template>
<template #append>
<q-icon v-if="filter !== ''" name="mdi-close" class="cursor-pointer" @click.stop="clearSearch()" />
<div class="flex items-center q-ml-sm">
<icon-info :customOptions="{ iconSize: '21px' }">
<div style="white-space: pre-line" v-html="translate('JS_FULL_TEXT_SEARCH_INFO')"></div>
</icon-info>
</div>
<div v-show="activeCategory !== ''" class="flex">
<q-toggle v-model="categorySearch" icon="mdi-file-tree" />
<q-tooltip>{{ translate('JS_KB_SEARCH_CURRENT_CATEGORY') }}</q-tooltip>
</div>
</template>
</q-input>
</div>
<div class="flex-md-grow-1 flex justify-end q-ml-sm-sm">
<q-btn round dense color="white" text-color="primary" icon="mdi-plus" @click="openQuickCreateModal()">
<q-tooltip>{{ translate('JS_KB_QUICK_CREATE') }}</q-tooltip>
</q-btn>
</div>
</q-toolbar>
</q-header>
<q-drawer
v-show="!searchData"
ref="drawer"
v-model="left"
:mini="miniState"
:width="searchData ? 0 : 250"
:breakpoint="700"
side="left"
elevated
content-class="bg-white text-black"
content-style="overflow: hidden !important"
>
<template v-if="showAccounts">
<q-tabs v-model="tab" class="text-grey" dense active-color="primary" indicator-color="primary" align="justify" narrow-indicator @input="onTabChange">
<q-tab name="categories" :label="translate('JS_KB_CATEGORIES')" />
<q-tab name="accounts" :label="translate('JS_KB_ACCOUNTS')" />
</q-tabs>
<q-tab-panels v-model="tab" style="height: calc(100% - 36px)" animated>
<q-tab-panel name="categories">
<q-scroll-area class="fit">
<categories-list :data="data" :activeCategory="activeCategory" @fetchData="fetchData" />
</q-scroll-area>
</q-tab-panel>
<q-tab-panel name="accounts">
<div class="q-px-sm">
<q-input v-model="accountSearch" :placeholder="translate('JS_KB_SEARCH_PLACEHOLDER')" dense>
<template #prepend>
<q-icon name="mdi-magnify" size="16px" />
</template>
<template #append>
<q-icon v-show="accountSearch !== ''" class="cursor-pointer" name="mdi-close" size="16px" @click="accountSearch = ''" />
</template>
</q-input>
</div>
<q-scroll-area style="height: calc(100% - 56px)">
<q-list>
<q-item
v-for="account in accountsList"
:key="account.id"
:active="activeAccount === account.name"
clickable
@click="
fetchData(null, account.id)
activeAccount = account.name
"
>
<q-item-section>{{ account.name }}</q-item-section>
<q-item-section avatar>
<a
class="js-popover-tooltip--record ellipsis"
:href="`index.php?module=Accounts&view=Detail&record=${account.id}`"
@click.prevent=""
>
<q-icon name="mdi-link" />
</a>
</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-tab-panel>
</q-tab-panels>
</template>
<q-scroll-area v-else class="fit">
<categories-list :data="data" :activeCategory="activeCategory" @fetchData="fetchData" />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-sm">
<div v-show="!searchData">
<columns-grid v-show="featuredCategories.length" :columnBlocks="featuredCategories" class="q-pa-sm">
<template #default="slotProps">
<q-list bordered padding dense>
<q-item header clickable class="text-black flex" @click="fetchData(slotProps.relatedBlock)">
<YfIcon :icon="tree.categories[slotProps.relatedBlock].icon" :size="iconSize" class="mr-2"></YfIcon>
{{ tree.categories[slotProps.relatedBlock].label }}
</q-item>
<q-item
v-for="featuredValue in selectedTabData.featured[slotProps.relatedBlock]"
:key="featuredValue.id"
class="text-subtitle2"
clickable
v-ripple
@click.prevent="showArticlePreview(featuredValue.id)"
>
<q-item-section class="align-items-center flex-row no-wrap justify-content-start">
<q-icon class="mr-2" name="mdi-star" :size="iconSize"></q-icon>
<a
class="js-popover-tooltip--record ellipsis"
:href="`index.php?module=${moduleName}&view=Detail&record=${featuredValue.id}`"
>{{ featuredValue.subject }}</a
>
</q-item-section>
</q-item>
</q-list>
</template>
</columns-grid>
<div v-show="activeCategory !== '' || tab === 'accounts'">
<q-separator v-show="featuredCategories.length" />
<articles-list :data="selectedTabData.records" :title="translate('JS_KB_ARTICLES')" @onClickRecord="previewDialog = true" />
</div>
</div>
<articles-list v-show="searchData" :data="searchDataArray" :title="translate('JS_KB_ARTICLES')" @onClickRecord="previewDialog = true" />
</q-page>
</q-page-container>
</q-layout>
<article-preview isDragResize :previewDialog.sync="previewDialog" @onDialogToggle="onDialogToggle" />
</div>
</template>
<script>
import YfIcon from '~/components/YfIcon.vue'
import IconInfo from '~/components/IconInfo.vue'
import ColumnsGrid from '~/components/ColumnsGrid.vue'
import Carousel from './components/Carousel.vue'
import ArticlesList from './components/ArticlesList.vue'
import ArticlePreview from './components/ArticlePreview.vue'
import CategoriesList from './components/CategoriesList.vue'
import { createNamespacedHelpers } from 'vuex'
const { mapGetters, mapActions } = createNamespacedHelpers('KnowledgeBase')
export default {
name: 'KnowledgeBase',
components: { YfIcon, IconInfo, Carousel, ArticlesList, ArticlePreview, ColumnsGrid, CategoriesList },
props: {
coordinates: {
type: Object,
default: () => {
return {
width: 0,
height: 0,
top: 0,
left: 0,
}
},
},
},
data() {
return {
drawerBehaviour: 'desktop',
miniState: false,
left: true,
filter: '',
accountSearch: '',
categorySearch: false,
searchData: false,
activeCategory: '',
activeAccount: '',
previewDialog: false,
tab: 'categories',
showAccounts: false,
data: {
categories: [],
records: [],
featured: {},
},
accountsData: {
categories: [],
records: [],
featured: {},
},
accounts: [],
}
},
computed: {
...mapGetters(['tree', 'record', 'iconSize', 'moduleName', 'maximized', 'defaultTreeIcon']),
accountsList() {
if (this.accountSearch === '') {
return this.accounts
} else {
return this.accounts.filter((account) => {
return account.name.toLowerCase().includes(this.accountSearch.toLowerCase())
})
}
},
selectedTabData() {
return this.tab === 'categories' ? this.data : this.accountsData
},
searchDataArray() {
return this.searchData ? this.searchData : []
},
featuredCategories() {
if (typeof this.selectedTabData.featured.length === 'undefined' && this.selectedTabData.categories) {
let arr = this.selectedTabData.categories.map((e) => {
return this.selectedTabData.featured[e] ? e : false
})
return arr.filter(function (item) {
return typeof item === 'string'
})
} else {
return []
}
},
inputFocus: {
set(val) {
return false
},
get() {
return this.filter.length > 0
},
},
},
methods: {
...mapActions(['fetchRecord', 'fetchCategories']),
onTabChange(tabName) {
if (this.accounts.length === 0 && tabName === 'accounts') {
this.fetchAccounts()
}
},
fetchAccounts() {
const aDeferred = $.Deferred()
const progressIndicatorElement = $.progressIndicator({
blockInfo: { enabled: true },
})
return AppConnector.request({
module: this.moduleName,
action: 'KnowledgeBaseAjax',
mode: 'getAccounts',
}).done((data) => {
let listData = data.result
if (listData) {
listData = Object.keys(listData).map(function (key) {
return { name: listData[key], id: key }
})
}
this.accounts = listData
progressIndicatorElement.progressIndicator({ mode: 'hide' })
aDeferred.resolve(listData)
})
},
search() {
if (this.filter.length >= 3) {
this.debouncedSearch()
} else {
this.searchData = false
}
},
clearSearch() {
this.filter = ''
this.searchData = false
},
fetchData(category = '', accountId = '') {
const aDeferred = $.Deferred()
if (category !== null) {
this.activeCategory = category
}
const progressIndicatorElement = $.progressIndicator({
blockInfo: { enabled: true },
})
return AppConnector.request({
module: this.moduleName,
action: 'KnowledgeBaseAjax',
mode: 'list',
category: category,
accountid: accountId,
}).done((data) => {
let listData = data.result
if (listData.showAccounts) {
this.showAccounts = true
}
if (listData.records) {
listData.records = Object.keys(listData.records).map(function (key) {
return { ...listData.records[key], id: key }
})
}
if (accountId !== '') {
this.accountsData = listData
} else {
this.data = listData
}
progressIndicatorElement.progressIndicator({ mode: 'hide' })
aDeferred.resolve(listData)
})
},
openQuickCreateModal() {
App.Components.QuickCreate.createRecord(this.moduleName)
},
showArticlePreview(id) {
this.fetchRecord(id).then(() => {
this.previewDialog = true
})
},
toggleDrawer() {
if (!this.$refs.drawer.belowBreakpoint) {
this.miniState = !this.miniState
} else {
this.left = !this.left
}
},
onDialogToggle(val) {
this.previewDialog = val
},
},
created() {
this.fetchCategories().then((_) => {
this.fetchData()
})
},
mounted() {
const debounceDelay = 1000
this.debouncedSearch = Quasar.utils.debounce(() => {
if (this.filter.length < 3) {
return
}
const aDeferred = $.Deferred()
const progressIndicatorElement = $.progressIndicator({
blockInfo: { enabled: true },
})
AppConnector.request({
module: this.moduleName,
action: 'KnowledgeBaseAjax',
mode: 'search',
value: this.filter,
category: this.categorySearch ? this.activeCategory : '',
}).done((data) => {
let listData = data.result
if (listData) {
listData = Object.keys(listData).map(function (key) {
return { ...listData[key], id: key }
})
}
this.searchData = listData
aDeferred.resolve(listData)
progressIndicatorElement.progressIndicator({ mode: 'hide' })
return listData
})
}, debounceDelay)
},
}
</script>
<style>
.tree-search {
min-width: 320px;
width: 50%;
}
.tree-search .q-field__control,
.tree-search .q-field__marginal {
height: 40px;
}
</style>