re-integrate backoffice search

This commit is contained in:
2022-02-04 15:59:35 +01:00
parent eb0ce9e145
commit 598d036d0b
23 changed files with 301 additions and 46 deletions
-11
View File
@@ -1,11 +0,0 @@
using zero.Api.Endpoints.Search;
namespace zero.Api.Configuration;
public class ApiOptions
{
/// <summary>
/// Configure search maps
/// </summary>
public SearchOptions Search { get; set; } = new();
}
@@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Filters;
using zero.Search;
namespace zero.Api.Endpoints.Search;
@@ -16,6 +16,10 @@ public class SearchController : ZeroApiController
[HttpGet("")]
public async Task<ActionResult<Paged>> Query([FromQuery] ListQuery<SearchResult> query)
{
if (!query.Search.HasValue())
{
return new Paged<SearchResult>(new List<SearchResult>(), 0, query.Page, query.PageSize);
}
return await Service.Query(query.Search);
}
}
@@ -8,13 +8,5 @@ public class SearchModule : ZeroModule
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IPermissionProvider, SearchPermissions>();
services.AddScoped<ISearchService, SearchService>();
//services.Configure<RavenOptions>(opts =>
//{
// opts.Indexes.Add<zero_Backoffice_Search>();
//});
services.AddOptions<SearchOptions>().Bind(configuration.GetSection("Zero:Search"));
}
}
+1 -3
View File
@@ -21,7 +21,6 @@ public class ZeroApiPlugin : ZeroPlugin
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ApiOptions>().Bind(configuration.GetSection("Zero:Api")).Configure<IWebHostEnvironment>(ConfigureOptions);
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroApiMvcOptions>());
services.AddTransient<IBackofficeApplicationResolverHandler, ApiApplicationResolverHandler>();
services.AddTransient<ApiUnhandledExceptionMiddleware>();
@@ -68,9 +67,8 @@ public class ZeroApiPlugin : ZeroPlugin
}
protected void ConfigureOptions(ApiOptions options, IWebHostEnvironment env)
protected void ConfigureOptions(IWebHostEnvironment env)
{
options.Search.Enabled = true;
//Map<Page>().Display((x, res, opts) =>
//{
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
-1
View File
@@ -59,7 +59,6 @@
{
await this.startup();
}
}
});
+3 -2
View File
@@ -22,7 +22,8 @@ import
integrationPlugin,
userPlugin,
linksPlugin,
pageModulePlugin
pageModulePlugin,
searchPlugin
} from '../modules';
import editorPlugin from '../editor/plugin';
import { ZeroSchema, ZeroSchemaExtension } from 'zero/schemas';
@@ -35,7 +36,7 @@ import { Emitter, EventType } from 'mitt';
plugins.push(
editorPlugin, countryPlugin, applicationPlugin, settingsPlugin, languagePlugin,
pagePlugin, linksPlugin, mediaPlugin, spacePlugin, mailTemplatePlugin,
translationPlugin, integrationPlugin, userPlugin, pageModulePlugin
translationPlugin, integrationPlugin, userPlugin, pageModulePlugin, searchPlugin
);
+2 -1
View File
@@ -11,4 +11,5 @@ export * from './translations';
export * from './integrations';
export * from './users';
export * from './links';
export * from './pageModules';
export * from './pageModules';
export * from './search';
@@ -0,0 +1,5 @@
import { get, post, put, del, ApiRequestConfig, ApiRequestQuery } from '../../services/request';
export default {
query: (term: string, query?: ApiRequestQuery, config?: ApiRequestConfig) => get("search", { ...config, params: { ...query, search: term } })
};
@@ -0,0 +1,45 @@
<template>
<ui-button icon="fth-search" :stroke="2.5" type="blank" class="app-nav-search" @click="openSearch" />
</template>
<script>
import * as overlays from '../../services/overlay';
export default {
data: () => ({
listener: null
}),
mounted()
{
this.listener = e =>
{
if (e.key === "f" && (e.ctrlKey || e.metaKey))
{
e.preventDefault();
this.openSearch();
}
};
document.addEventListener('keydown', this.listener.bind(this));
},
beforeDestroy()
{
document.removeEventListener('keydown', this.listener);
},
methods: {
async openSearch()
{
const result = await overlays.open({
component: () => import('./overlay.vue'),
autoclose: false,
softdismiss: true,
width: 780
//class: 'app-search-overlay'
});
}
}
}
</script>
@@ -0,0 +1,7 @@
import api from './api';
import plugin from './plugin';
export {
api as searchApi,
plugin as searchPlugin
};
@@ -0,0 +1,173 @@
<template>
<div class="app-search">
<form class="app-search-form" @submit.prevent="onSubmit">
<input ref="input" class="app-search-form-input" type="search" v-model="query" @input="onSearch" placeholder="Search..." />
<ui-button class="app-search-submit" :submit="true" type="blank" icon="fth-search" />
</form>
<div v-if="list.items.length" class="app-search-items">
<ui-link :to="item.url" v-for="item in list.items" :key="item.id" class="app-search-item" @click.native="config.close(true)">
<ui-icon :symbol="item.icon" :size="18" class="app-search-item-icon" />
<span class="app-search-item-text">
<span class="app-search-item-name">{{item.name}} <span class="app-search-item-group" v-localize="item.group"></span></span>
<span class="app-search-item-description" v-if="item.description">{{item.description}}</span>
</span>
</ui-link>
</div>
</div>
</template>
<script>
import api from './api';
import { debounce } from '../../utils';
export default {
props: {
model: Object,
config: Object
},
name: 'app-search',
data: () => ({
open: false,
query: null,
list: {
page: 1,
totalPages: 1,
items: []
},
onSearch: null
}),
created()
{
this.onSearch = debounce(this.search, 300);
},
mounted()
{
this.$nextTick(() =>
{
this.$refs.input.focus();
//this.$refs.input.select();
});
},
methods: {
async search()
{
const result = await api.query(this.query);
if (result.data)
{
this.list.items = result.data;
this.list.page = result.paging.page;
this.list.totalPages = result.paging.totalPages;
}
}
}
}
</script>
<style lang="scss">
.app-search-overlay .app-overlay
{
padding: var(--padding);
}
.app-search-dialog
{
}
.app-search-form
{
position: relative;
}
input.app-search-form-input
{
padding-right: 48px;
}
.app-search-submit
{
position: absolute;
right: 0;
height: 100%;
width: 48px;
justify-content: center;
}
.app-search-items
{
margin-top: var(--padding-s);
font-size: var(--font-size);
max-height: 490px;
overflow-y: auto;
}
.app-search-item
{
display: grid;
width: 100%;
grid-template-columns: 26px 1fr auto;
gap: 12px;
align-items: center;
position: relative;
color: var(--color-text);
padding: var(--padding-xs);
border-radius: var(--radius);
&:hover, &:focus
{
background: var(--color-tree-selected);
}
& + .app-search-item
{
margin-top: 2px;
}
}
.app-search-item-text
{
display: flex;
flex-direction: column;
position: relative;
top: 1px;
}
.app-search-item-name
{
font-size: var(--font-size);
/*display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;*/
}
.app-search-item-description
{
color: var(--color-text-dim);
margin-top: 3px;
}
.app-search-item-group
{
display: block;
font-size: var(--font-size-xs);
color: var(--color-text-dim);
margin-top: 3px;
//margin-left: 8px;
}
.app-search-item-icon
{
position: relative;
top: -1px;
left: 4px;
color: var(--color-text);
}
</style>
@@ -0,0 +1,11 @@
import { ZeroPlugin, ZeroPluginOptions } from '../../core';
import { defineAsyncComponent } from 'vue';
export default {
name: "zero.search",
install(app: ZeroPluginOptions)
{
app.vue.component('app-search', defineAsyncComponent(() => import('./button.vue')));
}
} as ZeroPlugin;
+1 -6
View File
@@ -7,7 +7,7 @@
<img src="/Assets/zero-dark.svg" class="show-dark" v-localize:alt="'@zero.name'" />
</h1>
<!--<ui-button icon="fth-search" :stroke="2.5" type="blank" class="app-nav-search" @click="openSearch" />-->
<app-search />
</div>
<div v-if="currentApplication && appStore.applications.length > 0" class="app-nav-switch">
@@ -173,11 +173,6 @@
{
this.compact = !this.compact;
localStorage.setItem(compactCacheKey, this.compact.toString());
},
openSearch()
{
//EventHub.$emit('app.search.open');
}
}
});
+1 -1
View File
@@ -68,7 +68,7 @@
this.rerender();
});
EventHub.$on('app.search.open', () =>
this.zero.events.on('app.search.open', () =>
{
this.search();
});
+14
View File
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using zero.Search;
namespace zero.Pages;
@@ -32,5 +33,18 @@ internal class ZeroPageModule : ZeroModule
cfg.Add<PageFolder>(Constants.Pages.FolderAlias, "@page.folder.name", "@page.folder.description", "fth-folder");
});
});
services.Configure<ZeroSearchOptions>(opts =>
{
opts.Map<Page>().Display((x, res, opts) =>
{
FlavorConfig flavor = opts.For<FlavorOptions>().Get<Page>(x.Flavor);
if (flavor != null)
{
res.Icon = flavor.Icon;
}
res.Url = "/pages/edit/" + x.Id;
});
});
}
}
@@ -1,14 +1,14 @@
using Raven.Client.Documents;
namespace zero.Api.Endpoints.Search;
namespace zero.Search;
public class zero_Backoffice_Search : ZeroJavascriptIndex
public class zero_Search : ZeroJavascriptIndex
{
public override void Setup(IZeroOptions options, IDocumentStore store)
{
// TODO index.Conventions is null, but needed for collection name retrieval
foreach (var map in options.For<SearchOptions>())
foreach (var map in options.For<ZeroSearchOptions>())
{
Maps.Add(map.BuildInstruction(store));
}
@@ -1,6 +1,6 @@
using Raven.Client.Documents;
namespace zero.Api.Endpoints.Search;
namespace zero.Search;
public class SearchIndexMap
{
@@ -66,7 +66,7 @@ public class SearchIndexMap<T> : SearchIndexMap where T : ZeroEntity
{
internal SearchIndexMap(string icon = null) : base(typeof(T), icon) { }
public SearchIndexMap<T> Icon(string icon)
public virtual SearchIndexMap<T> Icon(string icon)
{
_Icon = icon;
return this;
@@ -1,4 +1,4 @@
namespace zero.Api.Endpoints.Search;
namespace zero.Search;
public class SearchResult
{
@@ -2,7 +2,7 @@
using Raven.Client.Documents.Queries;
using Raven.Client.Documents.Session;
namespace zero.Api.Endpoints.Search;
namespace zero.Search;
public class SearchService : ISearchService
{
@@ -25,7 +25,7 @@ public class SearchService : ISearchService
return "*" + x + "*";
}).ToArray();
List<ZeroEntity> results = await Store.Session().Query<SearchIndexResult, zero_Backoffice_Search>()
List<ZeroEntity> results = await Store.Session().Query<SearchIndexResult, zero_Search>()
.Statistics(out QueryStatistics stats)
.Search(x => x.Name, searchParts, 2, @operator: SearchOperator.And)
.Search(x => x.Fields, searchParts, 1, Raven.Client.Documents.SearchOptions.Or, @operator: SearchOperator.And)
@@ -35,7 +35,7 @@ public class SearchService : ISearchService
List<SearchResult> items = new();
IEnumerable<SearchIndexMap> maps = Options.For<SearchOptions>();
IEnumerable<SearchIndexMap> maps = Options.For<ZeroSearchOptions>();
foreach (ZeroEntity result in results)
{
+19
View File
@@ -0,0 +1,19 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Search;
internal class ZeroSearchModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<ISearchService, SearchService>();
services.Configure<RavenOptions>(opts =>
{
opts.Indexes.Add<zero_Search>();
});
services.AddOptions<ZeroSearchOptions>().Bind(configuration.GetSection("Zero:Search"));
}
}
@@ -1,6 +1,6 @@
namespace zero.Api.Endpoints.Search;
namespace zero.Search;
public class SearchOptions : List<SearchIndexMap>
public class ZeroSearchOptions : List<SearchIndexMap>
{
public bool Enabled { get; set; }
+2 -1
View File
@@ -26,4 +26,5 @@ global using zero.Routing;
global using zero.Stores;
global using zero.Spaces;
global using zero.Utils;
global using zero.Validation;
global using zero.Validation;
global using zero.Search;
+1
View File
@@ -56,6 +56,7 @@ public class ZeroBuilder
Modules.Add<ZeroRoutingModule>();
Modules.Add<ZeroSpaceModule>();
Modules.Add<ZeroStoreModule>();
Modules.Add<ZeroSearchModule>();
Modules.ConfigureServices(services, configuration);