Skip to content

Commit

Permalink
chore: recover local search component (#4104)
Browse files Browse the repository at this point in the history
  • Loading branch information
SychO9 authored Nov 8, 2024
1 parent 04fe684 commit 8c33103
Show file tree
Hide file tree
Showing 17 changed files with 693 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type Mithril from 'mithril';

import app from '../app';
import highlight from '../../common/helpers/highlight';
import type { SearchSource } from './Search';
import type { GlobalSearchSource } from './GlobalSearch';
import extractText from '../../common/utils/extractText';
import Link from '../../common/components/Link';
import Icon from '../../common/components/Icon';
Expand All @@ -26,7 +26,7 @@ export class GeneralSearchResult {
/**
* Finds and displays settings, permissions and installed extensions (i.e. general search results) in the search dropdown.
*/
export default class GeneralSearchSource implements SearchSource {
export default class GeneralSearchSource implements GlobalSearchSource {
protected results = new Map<string, GeneralSearchResult[]>();

public resource: string = 'general';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import ItemList from '../../common/utils/ItemList';
import AbstractSearch, { type SearchAttrs, type SearchSource as BaseSearchSource } from '../../common/components/AbstractSearch';
import AbstractGlobalSearch, {
type SearchAttrs,
type GlobalSearchSource as BaseGlobalSearchSource,
} from '../../common/components/AbstractGlobalSearch';
import GeneralSearchSource from './GeneralSearchSource';
import app from '../app';

export interface SearchSource extends BaseSearchSource {}
export interface GlobalSearchSource extends BaseGlobalSearchSource {}

export default class Search extends AbstractSearch {
export default class GlobalSearch extends AbstractGlobalSearch {
static initAttrs(attrs: SearchAttrs) {
attrs.label = app.translator.trans('core.admin.header.search_placeholder', {}, true);
attrs.a11yRoleLabel = app.translator.trans('core.admin.header.search_role_label', {}, true);
}

sourceItems(): ItemList<SearchSource> {
const items = new ItemList<SearchSource>();
sourceItems(): ItemList<GlobalSearchSource> {
const items = new ItemList<GlobalSearchSource>();

items.add('general', new GeneralSearchSource());

Expand Down
4 changes: 2 additions & 2 deletions framework/core/js/src/admin/components/HeaderSecondary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SessionDropdown from './SessionDropdown';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import type Mithril from 'mithril';
import Search from './Search';
import GlobalSearch from './GlobalSearch';

/**
* The `HeaderSecondary` component displays secondary header controls.
Expand All @@ -21,7 +21,7 @@ export default class HeaderSecondary extends Component {
items() {
const items = new ItemList<Mithril.Children>();

items.add('search', <Search state={app.search.state} />, 30);
items.add('search', <GlobalSearch state={app.search.state} />, 30);

items.add(
'help',
Expand Down
2 changes: 1 addition & 1 deletion framework/core/js/src/admin/components/StatusWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import LoadingModal from './LoadingModal';
import LinkButton from '../../common/components/LinkButton';
import saveSettings from '../utils/saveSettings.js';
import saveSettings from '../utils/saveSettings';

export default class StatusWidget extends DashboardWidget {
className() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ export interface SearchAttrs extends ComponentAttrs {
}

/**
* The `SearchSource` interface defines a section of search results in the
* search dropdown.
* The `SearchSource` interface defines a tab of search results in the
* search modal.
*
* Search sources should be registered with the `Search` component class
* Search sources should be registered with the `GlobalSearch` component class
* by extending the `sourceItems` method. When the user types a
* query, each search source will be prompted to load search results via the
* `search` method. When the dropdown is redrawn, it will be constructed by
* `search` method. When the search modal's dropdown is redrawn, it will be constructed by
* putting together the output from the `view` method of each source.
*/
export interface SearchSource {
export interface GlobalSearchSource {
/**
* The resource type that this search source is responsible for.
*/
Expand Down Expand Up @@ -74,7 +74,7 @@ export interface SearchSource {
*
* Must be extended and the abstract methods implemented per-frontend.
*/
export default abstract class AbstractSearch<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
export default abstract class AbstractGlobalSearch<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
/**
* The instance of `SearchState` for this component.
*/
Expand Down Expand Up @@ -136,5 +136,5 @@ export default abstract class AbstractSearch<T extends SearchAttrs = SearchAttrs
/**
* A list of search sources that can be used to query for search results.
*/
abstract sourceItems(): ItemList<SearchSource>;
abstract sourceItems(): ItemList<GlobalSearchSource>;
}
10 changes: 5 additions & 5 deletions framework/core/js/src/common/components/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import LoadingIndicator from './LoadingIndicator';
import type IGambit from '../query/IGambit';
import ItemList from '../utils/ItemList';
import GambitsAutocomplete from '../utils/GambitsAutocomplete';
import type { SearchSource } from './AbstractSearch';
import type { GlobalSearchSource } from './AbstractGlobalSearch';

export interface ISearchModalAttrs extends IFormModalAttrs {
onchange: (value: string) => void;
searchState: SearchState;
sources: SearchSource[];
sources: GlobalSearchSource[];
}

export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearchModalAttrs> extends FormModal<CustomAttrs> {
Expand All @@ -32,12 +32,12 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
/**
* An array of SearchSources.
*/
protected sources!: SearchSource[];
protected sources!: GlobalSearchSource[];

/**
* The key of the currently-active search source.
*/
protected activeSource!: Stream<SearchSource>;
protected activeSource!: Stream<GlobalSearchSource>;

/**
* The sources that are still loading results.
Expand Down Expand Up @@ -214,7 +214,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
return items;
}

switchSource(source: SearchSource) {
switchSource(source: GlobalSearchSource) {
if (this.activeSource() !== source) {
this.activeSource(source);
this.search(this.query());
Expand Down
61 changes: 61 additions & 0 deletions framework/core/js/src/forum/components/DiscussionsSearchItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import app from '../../forum/app';
import Component, { ComponentAttrs } from '../../common/Component';
import Link from '../../common/components/Link';
import highlight from '../../common/helpers/highlight';
import Discussion from '../../common/models/Discussion';
import Post from '../../common/models/Post';
import type Mithril from 'mithril';
import ItemList from '../../common/utils/ItemList';

export interface DiscussionsSearchItemAttrs extends ComponentAttrs {
query: string;
discussion: Discussion;
mostRelevantPost: Post;
}

export default class DiscussionsSearchItem extends Component<DiscussionsSearchItemAttrs> {
query!: string;
discussion!: Discussion;
mostRelevantPost!: Post | null | undefined;

oninit(vnode: Mithril.Vnode<DiscussionsSearchItemAttrs, this>) {
super.oninit(vnode);

this.query = this.attrs.query;
this.discussion = this.attrs.discussion;
this.mostRelevantPost = this.attrs.mostRelevantPost;
}

view() {
return (
<li className="DiscussionSearchResult" data-index={'discussions' + this.discussion.id()}>
<Link href={app.route.discussion(this.discussion, (this.mostRelevantPost && this.mostRelevantPost.number()) || 0)}>
{this.viewItems().toArray()}
</Link>
</li>
);
}

discussionTitle() {
return this.discussion.title();
}

mostRelevantPostContent() {
return this.mostRelevantPost?.contentPlain();
}

viewItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add('discussion-title', <div className="DiscussionSearchResult-title">{highlight(this.discussionTitle(), this.query)}</div>, 90);

!!this.mostRelevantPost &&
items.add(
'most-relevant',
<div className="DiscussionSearchResult-excerpt">{highlight(this.mostRelevantPostContent() ?? '', this.query, 100)}</div>,
80
);

return items;
}
}
76 changes: 34 additions & 42 deletions framework/core/js/src/forum/components/DiscussionsSearchSource.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
import app from '../app';
import app from '../../forum/app';
import LinkButton from '../../common/components/LinkButton';
import { SearchSource } from './Search';
import type Mithril from 'mithril';
import type Discussion from '../../common/models/Discussion';
import type { SearchSource } from './Search';
import extractText from '../../common/utils/extractText';
import MinimalDiscussionListItem from './MinimalDiscussionListItem';
import Discussion from '../../common/models/Discussion';
import DiscussionsSearchItem from './DiscussionsSearchItem';

/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
* the search dropdown.
*/
export default class DiscussionsSearchSource implements SearchSource {
protected results = new Map<string, Discussion[]>();
queryString: string | null = null;

public resource: string = 'discussions';

title(): string {
return extractText(app.translator.trans('core.lib.search_source.discussions.heading'));
}

isCached(query: string): boolean {
return this.results.has(query.toLowerCase());
}

async search(query: string, limit: number): Promise<void> {
async search(query: string): Promise<void> {
query = query.toLowerCase();

this.results.set(query, []);

this.setQueryString(query);

const params = {
filter: { q: query },
page: { limit },
include: 'mostRelevantPost,user,firstPost,tags',
filter: { q: this.queryString || query },
page: { limit: this.limit() },
include: this.includes().join(','),
};

return app.store.find<Discussion[]>('discussions', params).then((results) => {
Expand All @@ -43,38 +35,38 @@ export default class DiscussionsSearchSource implements SearchSource {
view(query: string): Array<Mithril.Vnode> {
query = query.toLowerCase();

return (this.results.get(query) || []).map((discussion) => {
return (
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()} data-id={discussion.id()}>
<MinimalDiscussionListItem discussion={discussion} params={{ q: query }} />
</li>
);
}) as Array<Mithril.Vnode>;
}
this.setQueryString(query);

customGrouping(): boolean {
return false;
}
const results = (this.results.get(query) || []).map((discussion) => {
const mostRelevantPost = discussion.mostRelevantPost();

fullPage(query: string): Mithril.Vnode {
const filter = app.search.gambits.apply('discussions', { q: query });
const q = filter.q || null;
delete filter.q;
return <DiscussionsSearchItem query={query} discussion={discussion} mostRelevantPost={mostRelevantPost} />;
}) as Array<Mithril.Vnode>;

return (
return [
<li className="Dropdown-header">{app.translator.trans('core.lib.search_source.discussions.heading')}</li>,
<li>
<LinkButton icon="fas fa-search" href={app.route('index', { q, filter })}>
<LinkButton icon="fas fa-search" href={app.route('index', { q: this.queryString })}>
{app.translator.trans('core.lib.search_source.discussions.all_button', { query })}
</LinkButton>
</li>
);
</li>,
...results,
];
}

gotoItem(id: string): string | null {
const discussion = app.store.getById<Discussion>('discussions', id);
includes(): string[] {
return ['mostRelevantPost'];
}

if (!discussion) return null;
limit(): number {
return 3;
}

queryMutators(): string[] {
return [];
}

return app.route.discussion(discussion);
setQueryString(query: string): void {
this.queryString = query + ' ' + this.queryMutators().join(' ');
}
}
Loading

0 comments on commit 8c33103

Please sign in to comment.