mirror of https://github.com/Requarks/wiki.git
25 changed files with 766 additions and 108 deletions
Split View
Diff Options
-
1client/client-app.js
-
10client/components/admin/admin-contribute.vue
-
158client/components/admin/admin-search.vue
-
59client/components/common/nav-header.vue
-
169client/components/common/search-results.vue
-
15client/graph/common/common-pages-query-search.gql
-
24client/scss/global.scss
-
8client/static/svg/icon-no-results.svg
-
40client/static/svg/icon-selective-highlighting.svg
-
6client/store/site.js
-
1client/themes/default/components/page.vue
-
17dev/docker-postgres/docker-compose.yml
-
4package.json
-
2server/app/data.yml
-
1server/core/kernel.js
-
13server/graph/resolvers/page.js
-
20server/graph/schemas/page.graphql
-
13server/models/searchEngines.js
-
18server/modules/search/algolia/definition.yml
-
4server/modules/search/db/definition.yml
-
121server/modules/search/db/engine.js
-
39server/modules/search/elasticsearch/definition.yml
-
30server/modules/search/postgres/definition.yml
-
72server/modules/search/postgres/engine.js
-
29server/modules/search/solr/definition.yml
@ -0,0 +1,169 @@ |
|||
<template lang="pug"> |
|||
.search-results(v-if='search.length > 1') |
|||
.search-results-container |
|||
.search-results-loader(v-if='searchIsLoading && results.length < 1') |
|||
orbit-spinner( |
|||
:animation-duration='1000' |
|||
:size='100' |
|||
color='#FFF' |
|||
) |
|||
.headline.mt-5 Searching... |
|||
.search-results-none(v-if='!searchIsLoading && results.length < 1') |
|||
img(src='/svg/icon-no-results.svg', alt='No Results') |
|||
.subheading No pages matching your query. |
|||
template(v-if='results.length > 0') |
|||
v-subheader.white--text Found {{response.totalHits}} results |
|||
v-list.radius-7(two-line) |
|||
template(v-for='(item, idx) of results') |
|||
v-list-tile(@click='', :key='item.id') |
|||
v-list-tile-avatar(tile) |
|||
img(src='/svg/icon-selective-highlighting.svg') |
|||
v-list-tile-content |
|||
v-list-tile-title(v-html='item.title') |
|||
v-list-tile-sub-title(v-html='item.description') |
|||
.caption.grey--text.mt-1(v-html='item.path') |
|||
v-list-tile-action |
|||
v-chip(label) {{item.locale.toUpperCase()}} |
|||
v-divider(v-if='idx < results.length - 1') |
|||
v-pagination.mt-3( |
|||
v-if='paginationLength > 1' |
|||
dark |
|||
v-model='pagination' |
|||
:length='paginationLength' |
|||
) |
|||
template(v-if='suggestions.length > 0') |
|||
v-subheader.white--text.mt-3 Did you mean... |
|||
v-list.radius-7(dense, dark) |
|||
template(v-for='(term, idx) of suggestions') |
|||
v-list-tile(:key='term', @click='setSearchTerm(term)') |
|||
v-list-tile-avatar |
|||
v-icon search |
|||
v-list-tile-content |
|||
v-list-tile-title(v-html='term') |
|||
v-divider(v-if='idx < suggestions.length - 1') |
|||
.text-xs-center.pt-4 |
|||
v-btn(outline, color='orange', @click='search = ``', v-if='results.length > 0') |
|||
v-icon(left) save |
|||
span Copy Search Link |
|||
v-btn(outline, color='pink', @click='search = ``') |
|||
v-icon(left) clear |
|||
span Close |
|||
</template> |
|||
|
|||
<script> |
|||
import _ from 'lodash' |
|||
import { get, sync } from 'vuex-pathify' |
|||
import { OrbitSpinner } from 'epic-spinners' |
|||
|
|||
import searchPagesQuery from 'gql/common/common-pages-query-search.gql' |
|||
|
|||
export default { |
|||
components: { |
|||
OrbitSpinner |
|||
}, |
|||
data() { |
|||
return { |
|||
pagination: 1, |
|||
response: { |
|||
results: [], |
|||
suggestions: [], |
|||
totalHits: 0 |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
search: sync('site/search'), |
|||
searchIsLoading: sync('site/searchIsLoading'), |
|||
searchRestrictLocale: sync('site/searchRestrictLocale'), |
|||
searchRestrictPath: sync('site/searchRestrictPath'), |
|||
results() { |
|||
return this.response.results ? this.response.results : [] |
|||
}, |
|||
hits() { |
|||
return this.response.totalHits ? this.response.totalHits : 0 |
|||
}, |
|||
suggestions() { |
|||
return this.response.suggestions ? this.response.suggestions : [] |
|||
}, |
|||
paginationLength() { |
|||
return this.response.totalHits > 0 ? 0 : Math.ceil(this.response.totalHits / 10) |
|||
} |
|||
}, |
|||
methods: { |
|||
setSearchTerm(term) { |
|||
this.search = term |
|||
} |
|||
}, |
|||
apollo: { |
|||
response: { |
|||
query: searchPagesQuery, |
|||
variables() { |
|||
return { |
|||
query: this.search |
|||
} |
|||
}, |
|||
fetchPolicy: 'cache-and-network', |
|||
throttle: 1000, |
|||
skip() { |
|||
return !this.search || this.search.length < 2 |
|||
}, |
|||
update: (data) => _.get(data, 'pages.search', {}), |
|||
watchLoading (isLoading) { |
|||
this.searchIsLoading = isLoading |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.search-results { |
|||
position: fixed; |
|||
top: 64px; |
|||
left: 0; |
|||
width: 100%; |
|||
height: calc(100% - 64px); |
|||
background-color: rgba(0,0,0,.9); |
|||
z-index: 100; |
|||
text-align: center; |
|||
animation: searchResultsReveal .6s ease; |
|||
|
|||
@media #{map-get($display-breakpoints, 'sm-and-down')} { |
|||
top: 112px; |
|||
} |
|||
|
|||
&-container { |
|||
margin: 12px auto; |
|||
width: 90vw; |
|||
max-width: 1024px; |
|||
} |
|||
|
|||
&-loader { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
padding: 32px 0; |
|||
color: #FFF; |
|||
} |
|||
|
|||
&-none { |
|||
color: #FFF; |
|||
|
|||
img { |
|||
width: 200px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@keyframes searchResultsReveal { |
|||
0% { |
|||
background-color: rgba(0,0,0,0); |
|||
padding-top: 32px; |
|||
} |
|||
100% { |
|||
background-color: rgba(0,0,0,.9); |
|||
padding-top: 0; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,15 @@ |
|||
query ($query: String!) { |
|||
pages { |
|||
search(query:$query) { |
|||
results { |
|||
id |
|||
title |
|||
description |
|||
path |
|||
locale |
|||
} |
|||
suggestions |
|||
totalHits |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0"?> |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 192 192" width="64px" height="64px"> |
|||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"> |
|||
<path fill="#ffffff" d="M150.3,156.6h-108.6c-5.55,0 -10.65,-2.85 -13.35,-7.8c-2.7,-4.8 -2.7,-10.65 0.15,-15.45l54.3,-90.45c2.85,-4.65 7.65,-7.5 13.2,-7.5c5.55,0 10.35,2.85 13.2,7.5l54.15,90.45c2.85,4.8 3,10.65 0.15,15.45c-2.7,4.8 -7.65,7.8 -13.2,7.8zM96,44.4c-2.25,0 -4.2,1.05 -5.4,3.15l-54.3,90.45c-1.2,1.95 -1.2,4.35 -0.15,6.45c1.05,2.1 3.15,3.15 5.55,3.15h108.45c2.25,0 4.35,-1.2 5.55,-3.15c1.2,-1.95 1.05,-4.35 -0.15,-6.45l-54.15,-90.45c-1.2,-1.95 -3.15,-3.15 -5.4,-3.15z"/> |
|||
<path fill="#ffffff" d="M91.5,109.95c0,2.55 1.95,4.5 4.5,4.5v0c2.55,0 4.5,-1.95 4.5,-4.5v-31.5c0,-2.55 -1.95,-4.5 -4.5,-4.5v0c-2.55,0 -4.5,1.95 -4.5,4.5"/> |
|||
<circle fill="#ffffff" cx="64" cy="83.5" transform="scale(1.5,1.5)" r="3"/> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,40 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve" width="64px" height="64px"> |
|||
|
|||
<g> |
|||
<g> |
|||
</g> |
|||
</g> |
|||
<g> |
|||
<path d="M86,9H32c-5.5,0-10,4.5-10,10v90c0,5.5,4.5,10,10,10h64c5.5,0,10-4.5,10-10V29C106,18,97,9,86,9z" style="fill:#FFFFFF;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M55,62H42c-1.7,0-3-1.3-3-3s1.3-3,3-3h13c1.7,0,3,1.3,3,3S56.7,62,55,62z" style="fill:#E9EEF4;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M65,62c-0.2,0-0.4,0-0.6-0.1c-0.2,0-0.4-0.1-0.6-0.2c-0.2-0.1-0.3-0.2-0.5-0.3c-0.2-0.1-0.3-0.2-0.4-0.4 c-0.1-0.1-0.3-0.3-0.4-0.5c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.2-0.1-0.4-0.2-0.6c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.6,0.9-2.1 c0.1-0.1,0.3-0.3,0.4-0.4c0.2-0.1,0.3-0.2,0.5-0.3c0.2-0.1,0.4-0.1,0.6-0.2c1-0.2,2,0.1,2.7,0.8c0.6,0.6,0.9,1.3,0.9,2.1 c0,0.2,0,0.4-0.1,0.6c0,0.2-0.1,0.4-0.2,0.6s-0.2,0.4-0.3,0.5c-0.1,0.2-0.2,0.3-0.4,0.5C66.6,61.7,65.8,62,65,62z" style="fill:#FF5576;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M86,77H65c-1.7,0-3-1.3-3-3s1.3-3,3-3h21c1.7,0,3,1.3,3,3S87.7,77,86,77z" style="fill:#E9EEF4;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M86,62H75c-1.7,0-3-1.3-3-3s1.3-3,3-3h11c1.7,0,3,1.3,3,3S87.7,62,86,62z" style="fill:#E9EEF4;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M45,77h-3c-1.7,0-3-1.3-3-3s1.3-3,3-3h3c1.7,0,3,1.3,3,3S46.7,77,45,77z" style="fill:#E9EEF4;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M76.2,92h-34c-1.7,0-3-1.3-3-3s1.3-3,3-3h34c1.7,0,3,1.3,3,3S77.9,92,76.2,92z" style="fill:#E9EEF4;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M55,77c-0.8,0-1.6-0.3-2.1-0.9c-0.1-0.1-0.3-0.3-0.4-0.4c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.2-0.1-0.4-0.2-0.6 c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.6,0.9-2.1c0.7-0.7,1.7-1,2.7-0.8c0.2,0,0.4,0.1,0.6,0.2c0.2,0.1,0.4,0.2,0.5,0.3 c0.2,0.1,0.3,0.2,0.4,0.4c0.6,0.6,0.9,1.3,0.9,2.1s-0.3,1.6-0.9,2.1c-0.1,0.1-0.3,0.3-0.4,0.4c-0.2,0.1-0.3,0.2-0.5,0.3 c-0.2,0.1-0.4,0.1-0.6,0.2C55.4,77,55.2,77,55,77z" style="fill:#FF5576;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M86,92c-0.8,0-1.6-0.3-2.1-0.9c-0.1-0.1-0.3-0.3-0.4-0.5c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.2-0.1-0.4-0.2-0.6 c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.6,0.9-2.1c0.7-0.7,1.7-1,2.7-0.8c0.2,0,0.4,0.1,0.6,0.2c0.2,0.1,0.4,0.2,0.5,0.3 c0.2,0.1,0.3,0.2,0.5,0.4c0.6,0.6,0.9,1.3,0.9,2.1c0,0.2,0,0.4-0.1,0.6c0,0.2-0.1,0.4-0.2,0.6s-0.2,0.4-0.3,0.5 c-0.1,0.2-0.2,0.3-0.4,0.5c-0.1,0.1-0.3,0.3-0.5,0.4c-0.2,0.1-0.3,0.2-0.5,0.3c-0.2,0.1-0.4,0.1-0.6,0.2C86.4,92,86.2,92,86,92z" style="fill:#FF5576;"/> |
|||
</g> |
|||
<g> |
|||
<path d="M106,29H96c-5.5,0-10-4.5-10-10V9h0C97,9,106,18,106,29L106,29z" style="fill:#FFFFFF;"/> |
|||
</g> |
|||
<path d="M96,32h9.6c0.1,0,0.2,0,0.4,0c0,0,0,0,0,0c1.2,0,2.3-0.8,2.8-1.8c0.2-0.4,0.3-0.8,0.3-1.2c0-0.1,0-0.3,0-0.4 c-0.1-6-2.5-11.6-6.7-15.9C97.9,8.4,92.2,6,86,6c0,0,0,0,0,0H32c-7.2,0-13,5.8-13,13v90c0,7.3,5.9,13,11,13h63.8c8.7,0,15-5.5,15-13 l0.1-67c0-1.7-1.3-3-3-3c0,0,0,0,0,0c-1.7,0-3,1.3-3,3l-0.1,67c0,4.2-3.6,7-9,7H30c-1.7,0-5-3-5-7V19c0-3.9,3.1-7,7-7h51v7 C83,26.2,88.8,32,96,32z M102.7,26H96c-3.9,0-7-3.1-7-7v-6.7c3.4,0.6,6.5,2.2,9,4.7C100.5,19.5,102.1,22.6,102.7,26z" style="fill:#444B54;"/> |
|||
</svg> |
@ -1,6 +1,6 @@ |
|||
key: db |
|||
title: Database (built-in) |
|||
description: Default database-based search engine. |
|||
title: Database - Basic |
|||
description: Default basic database-based search engine. |
|||
author: requarks.io |
|||
logo: https://static.requarks.io/logo/database.svg |
|||
website: https://www.requarks.io/ |
|||
|
@ -1,26 +1,121 @@ |
|||
const _ = require('lodash') |
|||
|
|||
module.exports = { |
|||
activate() { |
|||
|
|||
// not used
|
|||
}, |
|||
deactivate() { |
|||
|
|||
// not used
|
|||
}, |
|||
query() { |
|||
|
|||
/** |
|||
* INIT |
|||
*/ |
|||
init() { |
|||
// not used
|
|||
}, |
|||
created() { |
|||
|
|||
/** |
|||
* SUGGEST |
|||
* |
|||
* @param {String} q Query |
|||
* @param {Object} opts Additional options |
|||
*/ |
|||
async suggest(q, opts) { |
|||
const results = await WIKI.models.pages.query() |
|||
.column('title') |
|||
.where(builder => { |
|||
builder.where('isPublished', true) |
|||
if (opts.locale) { |
|||
builder.andWhere('locale', opts.locale) |
|||
} |
|||
if (opts.path) { |
|||
builder.andWhere('path', 'like', `${opts.path}%`) |
|||
} |
|||
builder.andWhere('title', 'like', `%${q}%`) |
|||
}) |
|||
.limit(10) |
|||
return _.uniq(_.filter(_.flatten(results.map(r => r.title.split(' '))), w => w.indexOf(q) >= 0)) |
|||
}, |
|||
updated() { |
|||
/** |
|||
* QUERY |
|||
* |
|||
* @param {String} q Query |
|||
* @param {Object} opts Additional options |
|||
*/ |
|||
async query(q, opts) { |
|||
const results = await WIKI.models.pages.query() |
|||
.column('id', 'title', 'description', 'path', 'localeCode as locale') |
|||
.where(builder => { |
|||
builder.where('isPublished', true) |
|||
if (opts.locale) { |
|||
builder.andWhere('localeCode', opts.locale) |
|||
} |
|||
if (opts.path) { |
|||
builder.andWhere('path', 'like', `${opts.path}%`) |
|||
} |
|||
// TODO: Add user permissions filtering
|
|||
builder.andWhere(builder => { |
|||
switch(WIKI.config.db.type) { |
|||
case 'postgres': |
|||
builder.where('title', 'ILIKE', `%${q}%`) |
|||
builder.orWhere('description', 'ILIKE', `%${q}%`) |
|||
break |
|||
case 'mysql': |
|||
case 'mariadb': |
|||
builder.whereRaw(`title LIKE '%?%' COLLATE utf8_general_ci`, [q]) |
|||
builder.orWhereRaw(`description LIKE '%?%' COLLATE utf8_general_ci`, [q]) |
|||
break |
|||
|
|||
// TODO: MSSQL handling
|
|||
default: |
|||
builder.where('title', 'LIKE', `%${q}%`) |
|||
builder.orWhere('description', 'LIKE', `%${q}%`) |
|||
break |
|||
} |
|||
}) |
|||
}) |
|||
.limit(WIKI.config.search.maxHits) |
|||
return { |
|||
results, |
|||
suggestions: [], |
|||
totalHits: results.length |
|||
} |
|||
}, |
|||
deleted() { |
|||
|
|||
/** |
|||
* CREATE |
|||
* |
|||
* @param {Object} page Page to create |
|||
*/ |
|||
async created(page) { |
|||
// not used
|
|||
}, |
|||
renamed() { |
|||
|
|||
/** |
|||
* UPDATE |
|||
* |
|||
* @param {Object} page Page to update |
|||
*/ |
|||
async updated(page) { |
|||
// not used
|
|||
}, |
|||
rebuild() { |
|||
|
|||
/** |
|||
* DELETE |
|||
* |
|||
* @param {Object} page Page to delete |
|||
*/ |
|||
async deleted(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* RENAME |
|||
* |
|||
* @param {Object} page Page to rename |
|||
*/ |
|||
async renamed(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* REBUILD INDEX |
|||
*/ |
|||
async rebuild() { |
|||
// not used
|
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
key: postgres |
|||
title: Database - PostgreSQL |
|||
description: Advanced PostgreSQL-based search engine. |
|||
author: requarks.io |
|||
logo: https://static.requarks.io/logo/postgresql.svg |
|||
website: https://www.requarks.io/ |
|||
props: |
|||
dictLanguage: |
|||
type: String |
|||
title: Dictionnary Language |
|||
hint: Language to use when creating and querying text search vectors. |
|||
default: english |
|||
enum: |
|||
- simple |
|||
- danish |
|||
- dutch |
|||
- english |
|||
- finnish |
|||
- french |
|||
- german |
|||
- hungarian |
|||
- italian |
|||
- norwegian |
|||
- portuguese |
|||
- romanian |
|||
- russian |
|||
- spanish |
|||
- swedish |
|||
- turkish |
|||
order: 1 |
@ -0,0 +1,72 @@ |
|||
const _ = require('lodash') |
|||
|
|||
module.exports = { |
|||
activate() { |
|||
// not used
|
|||
}, |
|||
deactivate() { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* INIT |
|||
*/ |
|||
init() { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* SUGGEST |
|||
* |
|||
* @param {String} q Query |
|||
* @param {Object} opts Additional options |
|||
*/ |
|||
async suggest(q, opts) { |
|||
|
|||
}, |
|||
/** |
|||
* QUERY |
|||
* |
|||
* @param {String} q Query |
|||
* @param {Object} opts Additional options |
|||
*/ |
|||
async query(q, opts) { |
|||
|
|||
}, |
|||
/** |
|||
* CREATE |
|||
* |
|||
* @param {Object} page Page to create |
|||
*/ |
|||
async created(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* UPDATE |
|||
* |
|||
* @param {Object} page Page to update |
|||
*/ |
|||
async updated(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* DELETE |
|||
* |
|||
* @param {Object} page Page to delete |
|||
*/ |
|||
async deleted(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* RENAME |
|||
* |
|||
* @param {Object} page Page to rename |
|||
*/ |
|||
async renamed(page) { |
|||
// not used
|
|||
}, |
|||
/** |
|||
* REBUILD INDEX |
|||
*/ |
|||
async rebuild() { |
|||
// not used
|
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save