feat: admin search

pull/6775/head
NGPixel 2 years ago
parent a806aa3466
commit cbbc10dadb
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -116,8 +116,8 @@ export default {
const dbVersion = semver.coerce(resVersion.rows[0].server_version, { loose: true })
this.VERSION = dbVersion.version
this.LEGACY = dbVersion.major < 16
if (dbVersion.major < 11) {
WIKI.logger.error('Your PostgreSQL database version is too old and unsupported by Wiki.js. Exiting...')
if (dbVersion.major < 12) {
WIKI.logger.error(`Your PostgreSQL database version (${dbVersion.major}) is too old and unsupported by Wiki.js. Requires >= 12. Exiting...`)
process.exit(1)
}
WIKI.logger.info(`PostgreSQL ${dbVersion.version} [ ${this.LEGACY ? 'LEGACY MODE' : 'OK'} ]`)

@ -228,6 +228,7 @@ export async function up (knex) {
table.jsonb('relations').notNullable().defaultTo('[]')
table.text('content')
table.text('render')
table.specificType('ts', 'tsvector').index('ts_idx', { indexType: 'GIN' })
table.jsonb('toc')
table.string('editor').notNullable()
table.string('contentType').notNullable()
@ -488,6 +489,13 @@ export async function up (knex) {
dkimPrivateKey: ''
}
},
{
key: 'search',
value: {
termHighlighting: true,
dictOverrides: []
}
},
{
key: 'security',
value: {

@ -76,6 +76,9 @@ export default {
{ column: 'waitUntil', order: 'asc', nulls: 'first' },
{ column: 'createdAt', order: 'asc' }
])
},
systemSearch () {
return WIKI.config.search
}
},
Mutation: {
@ -179,6 +182,14 @@ export default {
operation: generateSuccess('System Flags applied successfully')
}
},
async updateSystemSearch (obj, args, context) {
WIKI.config.search = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.search)
// TODO: broadcast config update
await WIKI.configSvc.saveToDb(['search'])
return {
operation: generateSuccess('System Search configuration applied successfully')
}
},
async updateSystemSecurity (obj, args, context) {
WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
// TODO: broadcast config update

@ -13,6 +13,7 @@ extend type Query {
): [SystemJob]
systemJobsScheduled: [SystemJobScheduled]
systemJobsUpcoming: [SystemJobUpcoming]
systemSearch: SystemSearch
}
extend type Mutation {
@ -32,6 +33,11 @@ extend type Mutation {
id: UUID!
): DefaultResponse
updateSystemSearch(
termHighlighting: Boolean
dictOverrides: String
): DefaultResponse
updateSystemFlags(
flags: JSON!
): DefaultResponse
@ -213,3 +219,8 @@ type SystemCheckUpdateResponse {
latest: String
latestDate: String
}
type SystemSearch {
termHighlighting: Boolean
dictOverrides: String
}

@ -524,11 +524,16 @@
"admin.scheduler.useWorker": "Execution Mode",
"admin.scheduler.waitUntil": "Start",
"admin.search.configSaveSuccess": "Search engine configuration saved successfully.",
"admin.search.dictOverrides": "PostgreSQL Dictionary Mapping Overrides",
"admin.search.dictOverridesHint": "One override per line, in the format: en=english",
"admin.search.engineConfig": "Engine Configuration",
"admin.search.engineNoConfig": "This engine has no configuration options you can modify.",
"admin.search.highlighting": "Enable Term Highlighting",
"admin.search.highlightingHint": "Whether to show the highlighted terms in search results. There is a slight performance impact when enabled.",
"admin.search.indexRebuildSuccess": "Index rebuilt successfully.",
"admin.search.listRefreshSuccess": "List of search engines has been refreshed.",
"admin.search.rebuildIndex": "Rebuild Index",
"admin.search.saveSuccess": "Search engine configuration saved successfully",
"admin.search.searchEngine": "Search Engine",
"admin.search.subtitle": "Configure the search capabilities of your wiki",
"admin.search.title": "Search Engine",

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#199be2" d="M22.436,42.965l0.403-2.973c0.074-0.548-0.31-1.051-0.857-1.125 c-0.547-0.074-1.051,0.309-1.125,0.857l-0.403,2.973c-0.074,0.548,0.31,1.051,0.857,1.125S22.362,43.513,22.436,42.965z"/><path fill="#199be2" d="M17.554,41.905l1.163-2.765c0.214-0.51-0.025-1.095-0.534-1.309 c-0.509-0.214-1.095,0.024-1.309,0.534l-1.163,2.765c-0.214,0.51,0.025,1.095,0.534,1.309 C16.754,42.653,17.34,42.415,17.554,41.905z"/><path fill="#199be2" d="M25.922,38.878c-0.547,0.071-0.934,0.572-0.864,1.12l0.385,2.975 c0.071,0.548,0.572,0.934,1.12,0.864s0.934-0.572,0.864-1.12l-0.385-2.975C26.971,39.193,26.47,38.807,25.922,38.878z"/><path fill="#199be2" d="M29.686,37.882c-0.511,0.209-0.756,0.793-0.546,1.304l1.137,2.776 c0.21,0.512,0.794,0.756,1.304,0.546c0.511-0.209,0.756-0.793,0.546-1.304l-1.137-2.776C30.781,37.917,30.197,37.673,29.686,37.882 z"/><path fill="#199be2" d="M25.564,5.043l-0.403,2.973c-0.074,0.548,0.31,1.051,0.857,1.125 c0.547,0.074,1.051-0.309,1.125-0.857l0.403-2.973c0.074-0.548-0.31-1.051-0.857-1.125S25.638,4.495,25.564,5.043z"/><path fill="#199be2" d="M30.446,6.103l-1.163,2.765c-0.214,0.51,0.025,1.095,0.534,1.309 c0.509,0.214,1.095-0.024,1.309-0.534l1.163-2.765c0.214-0.51-0.025-1.095-0.534-1.309C31.246,5.355,30.66,5.593,30.446,6.103z"/><path fill="#199be2" d="M22.078,9.13c0.547-0.071,0.934-0.572,0.864-1.12l-0.385-2.975c-0.071-0.548-0.572-0.934-1.12-0.864 s-0.934,0.572-0.864,1.12l0.385,2.975C21.028,8.815,21.53,9.201,22.078,9.13z"/><path fill="#199be2" d="M18.314,10.126c0.511-0.209,0.756-0.793,0.546-1.304l-1.137-2.776 c-0.21-0.512-0.794-0.756-1.304-0.546c-0.511,0.209-0.756,0.793-0.546,1.304L17.01,9.58C17.219,10.091,17.803,10.335,18.314,10.126 z"/><path fill="#199be2" d="M33.55,35.722c0.018-0.018,0.037-0.036,0.057-0.052c6.186-5.085,7.33-14.153,2.555-20.631 c0.021-0.005,0.041-0.006,0.063-0.011c1.606-0.379,2.99-1.282,4.201-2.4c5.963,8.602,4.295,20.397-3.906,26.971 c-0.221,0.177-0.547,0.134-0.719-0.092c0,0-2.319-3.047-2.322-3.051C33.294,36.216,33.353,35.922,33.55,35.722z"/><path fill="#199be2" d="M35.796,11.445l-0.881,6.486c-0.058,0.427,0.43,0.711,0.773,0.45l7.913-6.021 c0.342-0.26,0.198-0.806-0.229-0.864l-6.486-0.881C36.356,10.544,35.868,10.915,35.796,11.445z"/><path fill="#199be2" d="M14.44,12.291c-0.019,0.017-0.039,0.034-0.058,0.051c-6.186,5.085-7.33,14.153-2.555,20.631 c-0.021,0.005-0.041,0.006-0.063,0.011c-1.606,0.379-2.99,1.282-4.201,2.4c-5.963-8.602-4.295-20.397,3.906-26.971 c0.221-0.177,0.547-0.134,0.719,0.092l2.322,3.051c0.084,0.133,0.146,0.265,0.119,0.426C14.608,12.11,14.532,12.207,14.44,12.291z"/><path fill="#199be2" d="M12.192,36.568l0.881-6.486c0.058-0.427-0.43-0.711-0.773-0.45l-7.913,6.021 c-0.342,0.26-0.198,0.806,0.229,0.864l6.486,0.881C11.632,37.468,12.12,37.098,12.192,36.568z"/><path fill="#199be2" d="M33.371,35.865c-0.468,0.429-0.486,1.161-0.039,1.613l10.133,10.229c0.391,0.391,1.024,0.391,1.414,0 l2.828-2.828c0.391-0.391,0.391-1.024,0-1.414L36.936,32.596L33.371,35.865z"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

@ -43,7 +43,14 @@ q-header.bg-header.text-white.site-header(
@blur='state.searchKbdShortcutShown = true'
)
template(v-slot:prepend)
q-icon(name='las la-search')
q-circular-progress.q-mr-xs(
v-if='siteStore.searchIsLoading'
indeterminate
rounded
color='primary'
size='20px'
)
q-icon(v-else, name='las la-search')
template(v-slot:append)
q-badge.q-mr-sm(
v-if='state.searchKbdShortcutShown'
@ -176,7 +183,10 @@ function handleKeyPress (ev) {
}
function onSearchEnter () {
if (!route.path.startsWith('/_search')) {
if (route.path === '/_search') {
router.replace({ path: '/_search', query: { q: siteStore.search } })
} else {
siteStore.searchIsLoading = true
router.push({ path: '/_search', query: { q: siteStore.search } })
}
}

@ -187,6 +187,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section {{ t('admin.scheduler.title') }}
q-item-section(side)
status-light(:color='adminStore.info.isSchedulerHealthy ? `positive` : `warning`', :pulse='!adminStore.info.isSchedulerHealthy')
q-item(to='/_admin/search', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-find-and-replace.svg')
q-item-section {{ t('admin.search.title') }}
q-item(to='/_admin/security', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-protect.svg')

@ -0,0 +1,193 @@
<template lang='pug'>
q-page.admin-flags
.row.q-pa-md.items-center
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-find-and-replace.svg')
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.search.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.search.subtitle') }}
.col-auto
q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle'
flat
color='grey'
:aria-label='t(`common.actions.viewDocs`)'
:href='siteStore.docsBase + `/system/search`'
target='_blank'
type='a'
)
q-tooltip {{ t(`common.actions.viewDocs`) }}
q-btn.q-mr-sm.acrylic-btn(
icon='las la-redo-alt'
flat
color='secondary'
:loading='state.loading > 0'
:aria-label='t(`common.actions.refresh`)'
@click='load'
)
q-tooltip {{ t(`common.actions.refresh`) }}
q-btn(
unelevated
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
:loading='state.loading > 0'
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
.col-12.col-lg-7
q-card.q-py-sm
q-item(tag='label')
blueprint-icon(icon='search')
q-item-section
q-item-label {{t(`admin.search.highlighting`)}}
q-item-label(caption) {{t(`admin.search.highlightingHint`)}}
q-item-section(avatar)
q-toggle(
v-model='state.config.termHighlighting'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.search.highlighting`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon.self-start(icon='search')
q-item-section
q-item-label {{t(`admin.search.dictOverrides`)}}
q-input.q-mt-sm(
type='textarea'
v-model='state.config.dictOverrides'
outlined
:aria-label='t(`admin.search.dictOverrides`)'
:hint='t(`admin.search.dictOverridesHint`)'
input-style='min-height: 200px;'
)
.col-12.col-lg-5.gt-md
.q-pa-md.text-center
img(src='/_assets/illustrations/undraw_file_searching.svg', style='width: 80%;')
</template>
<script setup>
import gql from 'graphql-tag'
import { onMounted, reactive, ref } from 'vue'
import { cloneDeep, omit } from 'lodash-es'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useSiteStore } from 'src/stores/site'
import { useFlagsStore } from 'src/stores/flags'
// QUASAR
const $q = useQuasar()
// STORES
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('admin.flags.title')
})
// DATA
const state = reactive({
loading: 0,
config: {
termHighlighting: false,
dictOverrides: ''
}
})
// METHODS
async function load () {
state.loading++
$q.loading.show()
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSearchConfig {
systemSearch {
termHighlighting
dictOverrides
}
}
`,
fetchPolicy: 'network-only'
})
state.config = cloneDeep(resp?.data?.systemSearch)
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to load search config',
caption: err.message
})
}
$q.loading.hide()
state.loading--
}
async function save () {
state.loading++
try {
const respRaw = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation saveSearchConfig (
$termHighlighting: Boolean
$dictOverrides: String
) {
updateSystemSearch(
termHighlighting: $termHighlighting
dictOverrides: $dictOverrides
) {
operation {
succeeded
slug
message
}
}
}
`,
variables: state.config
})
const resp = respRaw?.data?.updateSystemSearch?.operation || {}
if (resp.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.search.saveSuccess')
})
} else {
throw new Error(resp.message)
}
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to save search config',
caption: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(async () => {
load()
})
</script>
<style lang='scss'>
</style>

@ -94,9 +94,9 @@ q-layout(view='hHh Lpr lff')
.text-header.flex
span {{t('search.results')}}
q-space
span.text-caption #[strong 12] results
span.text-caption #[strong {{ state.items }}] results
q-list(separator, padding)
q-item(v-for='item of 12', clickable)
q-item(v-for='item of state.items', clickable)
q-item-section(avatar)
q-avatar(color='primary' text-color='white' rounded icon='las la-file-alt')
q-item-section
@ -169,7 +169,8 @@ const state = reactive({
filterTags: [],
filterLocale: ['en'],
filterEditor: '',
filterPublishState: ''
filterPublishState: '',
items: 25
})
const editors = computed(() => {
@ -205,6 +206,13 @@ function pageStyle (offset, height) {
'min-height': `${height - 100 - offset}px`
}
}
// MOUNTED
onMounted(() => {
siteStore.searchIsLoading = false
})
</script>
<style lang="scss">

@ -63,6 +63,7 @@ const routes = [
{ path: 'mail', component: () => import('pages/AdminMail.vue') },
{ path: 'rendering', component: () => import('pages/AdminRendering.vue') },
{ path: 'scheduler', component: () => import('pages/AdminScheduler.vue') },
{ path: 'search', component: () => import('pages/AdminSearch.vue') },
{ path: 'security', component: () => import('pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('pages/AdminSystem.vue') },
{ path: 'terminal', component: () => import('pages/AdminTerminal.vue') },

@ -16,10 +16,7 @@ export const useSiteStore = defineStore('site', {
description: '',
logoText: true,
search: '',
searchIsFocused: false,
searchIsLoading: false,
searchRestrictLocale: false,
searchRestrictPath: false,
printView: false,
pageDataTemplates: [],
showSideNav: true,

Loading…
Cancel
Save