Skip to content

Commit

Permalink
feat: search
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipinho committed Jan 29, 2024
1 parent e0e5f7c commit a0ec2f3
Show file tree
Hide file tree
Showing 22 changed files with 522 additions and 174 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The server will be available on `http://localhost:3000`
$ pnpm nx run server:migration:create init

# Generates 'init' migration file from existing entities to update the database schema
$ pnpm nx run server::generate init
$ pnpm nx run server:migration:generate init

# Runs all pending migrations to update the database schema
$ pnpm nx run server:migration:run
Expand Down
11 changes: 11 additions & 0 deletions apps/client/src/features/search/queries/search-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { searchPage } from '@/features/search/services/search-service';
import { IPageSearch } from '@/features/search/types/search.types';

export function usePageSearchQuery(query: string): UseQueryResult<IPageSearch[], Error> {
return useQuery({
queryKey: ['page-history', query],
queryFn: () => searchPage(query),
enabled: !!query,
});
}
88 changes: 52 additions & 36 deletions apps/client/src/features/search/search-spotlight.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,59 @@
import { rem } from '@mantine/core';
import { Spotlight, SpotlightActionData } from '@mantine/spotlight';
import { IconHome, IconDashboard, IconSettings, IconSearch } from '@tabler/icons-react';

const actions: SpotlightActionData[] = [
{
id: 'home',
label: 'Home',
description: 'Get to home page',
onClick: () => console.log('Home'),
leftSection: <IconHome style={{ width: rem(24), height: rem(24) }} stroke={1.5} />,
},
{
id: 'dashboard',
label: 'Dashboard',
description: 'Get full information about current system status',
onClick: () => console.log('Dashboard'),
leftSection: <IconDashboard style={{ width: rem(24), height: rem(24) }} stroke={1.5} />,
},
{
id: 'settings',
label: 'Settings',
description: 'Account settings and workspace management',
onClick: () => console.log('Settings'),
leftSection: <IconSettings style={{ width: rem(24), height: rem(24) }} stroke={1.5} />,
},
];
import { Group, Center, Text } from '@mantine/core';
import { Spotlight } from '@mantine/spotlight';
import { IconFileDescription, IconHome, IconSearch, IconSettings } from '@tabler/icons-react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDebouncedValue } from '@mantine/hooks';
import { usePageSearchQuery } from '@/features/search/queries/search-query';


export function SearchSpotlight() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [debouncedSearchQuery] = useDebouncedValue(query, 300);
const { data: searchResults, isLoading, error } = usePageSearchQuery(debouncedSearchQuery)

const items = (searchResults && searchResults.length > 0 ? searchResults : [])
.map((item) => (
<Spotlight.Action key={item.title} onClick={() => navigate(`/p/${item.id}`)}>
<Group wrap="nowrap" w="100%">
<Center>
{item?.icon ? (
<span style={{ fontSize: "20px" }}>{ item.icon }</span>
) : (
<IconFileDescription size={20} />
)}
</Center>

<div style={{ flex: 1 }}>
<Text>{item.title}</Text>

{item?.highlight && (
<Text opacity={0.6} size="xs" dangerouslySetInnerHTML={{ __html: item.highlight }}/>
)}
</div>

</Group>
</Spotlight.Action>
));

return (
<>
<Spotlight
actions={actions}
nothingFound="Nothing found..."
highlightQuery
searchProps={{
leftSection: <IconSearch style={{ width: rem(20), height: rem(20) }} stroke={1.5} />,
placeholder: 'Search...',
}}
/>
<Spotlight.Root query={query}
onQueryChange={setQuery}
scrollable
overlayProps={{
backgroundOpacity: 0.55,
}}>
<Spotlight.Search placeholder="Search..."
leftSection={
<IconSearch size={20} stroke={1.5} />
} />
<Spotlight.ActionsList>
{items.length > 0 ? items : <Spotlight.Empty>No results found...</Spotlight.Empty>}
</Spotlight.ActionsList>
</Spotlight.Root>

</>
);
}
7 changes: 7 additions & 0 deletions apps/client/src/features/search/services/search-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import api from '@/lib/api-client';
import { IPageSearch } from '@/features/search/types/search.types';

export async function searchPage(query: string): Promise<IPageSearch[]> {
const req = await api.post<IPageSearch[]>('/search', { query });
return req.data as any;
}
12 changes: 12 additions & 0 deletions apps/client/src/features/search/types/search.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

export interface IPageSearch {
id: string;
title: string;
icon: string;
parentPageId: string;
creatorId: string;
createdAt: Date;
updatedAt: Date;
rank: string;
highlight: string;
}
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"fs-extra": "^11.1.1",
"mime-types": "^2.1.35",
"pg": "^8.11.3",
"pg-tsquery": "^8.4.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"sanitize-filename-ts": "^1.0.2",
Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/collaboration/collaboration.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { StarterKit } from '@tiptap/starter-kit';
import { TextAlign } from '@tiptap/extension-text-align';
import { TaskList } from '@tiptap/extension-task-list';
import { TaskItem } from '@tiptap/extension-task-item';
import { Underline } from '@tiptap/extension-underline';
import { Link } from '@tiptap/extension-link';
import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Highlight } from '@tiptap/extension-highlight';
import { Typography } from '@tiptap/extension-typography';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
import { TrailingNode, Comment } from '@docmost/editor-ext';
import { generateHTML, generateJSON } from '@tiptap/html';
import { generateText, JSONContent } from '@tiptap/core';

export const tiptapExtensions = [
StarterKit,
Comment,
TextAlign,
TaskList,
TaskItem,
Underline,
Link,
Superscript,
SubScript,
Highlight,
Typography,
TrailingNode,
TextStyle,
Color,
];

export function jsonToHtml(tiptapJson: JSONContent) {
return generateHTML(tiptapJson, tiptapExtensions);
}

export function htmlToJson(html: string) {
return generateJSON(html, tiptapExtensions);
}

export function jsonToText(tiptapJson: JSONContent) {
return generateText(tiptapJson, tiptapExtensions);
}
47 changes: 17 additions & 30 deletions apps/server/src/collaboration/extensions/persistence.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,8 @@ import * as Y from 'yjs';
import { PageService } from '../../core/page/services/page.service';
import { Injectable } from '@nestjs/common';
import { TiptapTransformer } from '@hocuspocus/transformer';
import { StarterKit } from '@tiptap/starter-kit';
import { TextAlign } from '@tiptap/extension-text-align';
import { TaskList } from '@tiptap/extension-task-list';
import { TaskItem } from '@tiptap/extension-task-item';
import { Underline } from '@tiptap/extension-underline';
import { Link } from '@tiptap/extension-link';
import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Highlight } from '@tiptap/extension-highlight';
import { Typography } from '@tiptap/extension-typography';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
import { TrailingNode, Comment } from '@docmost/editor-ext';
import { jsonToHtml, jsonToText, tiptapExtensions } from '../collaboration.util';
import { generateText } from '@tiptap/core'

@Injectable()
export class PersistenceExtension implements Extension {
Expand Down Expand Up @@ -55,22 +44,12 @@ export class PersistenceExtension implements Extension {
if (page.content) {
console.log('converting json to ydoc');

const ydoc = TiptapTransformer.toYdoc(page.content, 'default', [
StarterKit,
Comment,
TextAlign,
TaskList,
TaskItem,
Underline,
Link,
Superscript,
SubScript,
Highlight,
Typography,
TrailingNode,
TextStyle,
Color,
]);
const ydoc = TiptapTransformer.toYdoc(
page.content,
'default',
tiptapExtensions,
);

Y.encodeStateAsUpdate(ydoc);
return ydoc;
}
Expand All @@ -87,8 +66,16 @@ export class PersistenceExtension implements Extension {
const tiptapJson = TiptapTransformer.fromYdoc(document, 'default');
const ydocState = Buffer.from(Y.encodeStateAsUpdate(document));

const textContent = jsonToText(tiptapJson);
console.log(jsonToText(tiptapJson));

try {
await this.pageService.updateState(pageId, tiptapJson, ydocState);
await this.pageService.updateState(
pageId,
tiptapJson,
textContent,
ydocState,
);
} catch (err) {
console.error(`Failed to update page ${documentName}`);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { StorageModule } from './storage/storage.module';
import { AttachmentModule } from './attachment/attachment.module';
import { EnvironmentModule } from '../environment/environment.module';
import { CommentModule } from './comment/comment.module';
import { SearchModule } from './search/search.module';

@Module({
imports: [
Expand All @@ -19,6 +20,7 @@ import { CommentModule } from './comment/comment.module';
}),
AttachmentModule,
CommentModule,
SearchModule,
],
})
export class CoreModule {}
19 changes: 16 additions & 3 deletions apps/server/src/core/page/entities/page.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,34 @@ export class Page {
@Column({ length: 500, nullable: true })
title: string;

@Column({ nullable: true })
icon: string;

@Column({ type: 'jsonb', nullable: true })
content: string;

@Column({ type: 'text', nullable: true })
html: string;

@Column({ type: 'text', nullable: true })
textContent: string;

@Column({
type: 'tsvector',
generatedType: 'STORED',
asExpression:
"setweight(to_tsvector('english', coalesce(pages.title, '')), 'A') || setweight(to_tsvector('english', coalesce(pages.\"textContent\", '')), 'B')",
select: false,
nullable: true,
})
tsv: string;

@Column({ type: 'bytea', nullable: true })
ydoc: any;

@Column({ nullable: true })
slug: string;

@Column({ nullable: true })
icon: string;

@Column({ nullable: true })
coverPhoto: string;

Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/core/page/page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import { PageHistoryRepository } from './repositories/page-history.repository';
PageRepository,
PageHistoryRepository,
],
exports: [PageService, PageOrderingService, PageHistoryService],
exports: [
PageService,
PageOrderingService,
PageHistoryService,
PageRepository,
],
})
export class PageModule {}
2 changes: 2 additions & 0 deletions apps/server/src/core/page/services/page.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,13 @@ export class PageService {
async updateState(
pageId: string,
content: any,
textContent: string,
ydoc: any,
userId?: string, // TODO: fix this
): Promise<void> {
await this.pageRepository.update(pageId, {
content: content,
textContent: textContent,
ydoc: ydoc,
...(userId && { lastUpdatedById: userId }),
});
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/core/search/dto/search-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class SearchResponseDto {
id: string;
title: string;
icon: string;
parentPageId: string;
creatorId: string;
rank: string;
highlight: string;
}
18 changes: 18 additions & 0 deletions apps/server/src/core/search/dto/search.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IsNumber, IsOptional, IsString } from 'class-validator';

export class SearchDTO {
@IsString()
query: string;

@IsOptional()
@IsString()
creatorId?: string;

@IsOptional()
@IsNumber()
limit?: number;

@IsOptional()
@IsNumber()
offset?: number;
}
18 changes: 18 additions & 0 deletions apps/server/src/core/search/search.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SearchController } from './search.controller';

describe('SearchController', () => {
let controller: SearchController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SearchController],
}).compile();

controller = module.get<SearchController>(SearchController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Loading

0 comments on commit a0ec2f3

Please sign in to comment.