mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
* advanced search filters * improve labelId filtering in query * index label ids on typesense * index label ids on typesense * filters on AI search * refactor: detect typesense schema drift from the declared collection schema * feat: show secondary search filters only when applied Created by and Labels now stay hidden until they hold a value; a Filter button at the right end of the bar lists the hidden ones and opens the picked filter's dropdown immediately. * fix: keep applied search filters in the order they were added * feat: add title-only search mode Adds a titleOnly flag to page search: the Postgres driver matches titles via trigram-indexed unaccented LIKE ranked by word_similarity, and a Match filter in the search spotlight switches between everything and title-only matching. * refactor: make title-only search a toggle chip * fix: drop stale results when the search content type changes * feat: add trigram index on attachment file names * feat: extend title-only search to attachments The titleOnly flag now matches attachment file names via the separator-normalized trigram expression, and the toggle chip stays available in attachment mode as File name only. * refactor: keep the title-only label uniform across search modes * feat: index txt attachments for content search * feat: pin the current user at the top of the creator filter * refactor: dim the you suffix in the creator filter * feat: browse label-filtered pages without a query Selecting labels now lists their pages newest-first before any text is typed, on both the Postgres and Typesense drivers. * fix: match any selected label instead of requiring all * fix: use a gray spotlight selection instead of primary blue * feat: include search results in the spotlight tab order * feat: show last updated time on search results * fix: stop empty-state flicker while the search query debounces * feat: browse creator-filtered results without a query Selecting a creator with an empty query now lists their pages (or attachments in attachment mode) newest-first, with the same space and page permission filtering as typed search. * fix: show Untitled for pages without a title in search results * quick page sharing fix * minor search filter fixes --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
155 lines
4.0 KiB
TypeScript
155 lines
4.0 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { Label } from '@docmost/db/types/entity.types';
|
|
import { LabelRepo, LabelType } from '@docmost/db/repos/label/label.repo';
|
|
import { InjectKysely } from 'nestjs-kysely';
|
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
|
import { executeTx } from '@docmost/db/utils';
|
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
|
import { normalizeLabelName } from './utils';
|
|
import { EventEmitter2 } from "@nestjs/event-emitter";
|
|
import { EventName } from "src/common/events/event.contants";
|
|
|
|
@Injectable()
|
|
export class LabelService {
|
|
constructor(
|
|
private readonly labelRepo: LabelRepo,
|
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
|
private readonly eventEmitter: EventEmitter2,
|
|
@InjectKysely() private readonly db: KyselyDB,
|
|
) {}
|
|
|
|
async addLabelsToPage(
|
|
pageId: string,
|
|
names: string[],
|
|
workspaceId: string,
|
|
): Promise<Label[]> {
|
|
const attached: Label[] = [];
|
|
await executeTx(this.db, async (trx) => {
|
|
for (const name of names) {
|
|
const label = await this.labelRepo.findOrCreate(
|
|
name.trim(),
|
|
workspaceId,
|
|
LabelType.PAGE,
|
|
trx,
|
|
);
|
|
await this.labelRepo.addLabelToPage(pageId, label.id, trx);
|
|
attached.push(label);
|
|
}
|
|
});
|
|
|
|
this.eventEmitter.emit(EventName.PAGE_UPDATED, {
|
|
pageIds: [pageId],
|
|
workspaceId: workspaceId,
|
|
});
|
|
|
|
return attached;
|
|
}
|
|
|
|
async removeLabelFromPage(
|
|
pageId: string,
|
|
labelId: string,
|
|
workspaceId: string,
|
|
): Promise<void> {
|
|
await executeTx(this.db, async (trx) => {
|
|
const label = await this.labelRepo.findById(labelId, trx);
|
|
if (!label || label.workspaceId !== workspaceId) {
|
|
throw new NotFoundException('Label not found');
|
|
}
|
|
|
|
await this.labelRepo.removeLabelFromPage(
|
|
pageId,
|
|
labelId,
|
|
workspaceId,
|
|
trx,
|
|
);
|
|
|
|
const count = await this.labelRepo.getLabelPageCount(
|
|
labelId,
|
|
workspaceId,
|
|
trx,
|
|
);
|
|
if (count === 0) {
|
|
await this.labelRepo.deleteLabel(labelId, workspaceId, trx);
|
|
}
|
|
});
|
|
|
|
this.eventEmitter.emit(EventName.PAGE_UPDATED, {
|
|
pageIds: [pageId],
|
|
workspaceId: workspaceId,
|
|
});
|
|
}
|
|
|
|
async getPageLabels(pageId: string, pagination: PaginationOptions) {
|
|
return this.labelRepo.findLabelsByPageId(pageId, pagination);
|
|
}
|
|
|
|
async getLabels(
|
|
workspaceId: string,
|
|
userId: string,
|
|
type: LabelType,
|
|
pagination: PaginationOptions,
|
|
) {
|
|
return this.labelRepo.findLabels(
|
|
workspaceId,
|
|
userId,
|
|
type,
|
|
pagination,
|
|
);
|
|
}
|
|
|
|
async findPagesByLabel(
|
|
labelId: string,
|
|
userId: string,
|
|
opts: {
|
|
spaceId?: string;
|
|
query?: string;
|
|
pagination: PaginationOptions;
|
|
},
|
|
) {
|
|
const result = await this.labelRepo.findPagesByLabelId(labelId, userId, opts);
|
|
if (result.items.length === 0) return result;
|
|
|
|
const accessibleIds = await this.pagePermissionRepo.filterAccessiblePageIds({
|
|
pageIds: result.items.map((p) => p.id),
|
|
userId,
|
|
spaceId: opts.spaceId,
|
|
});
|
|
const accessible = new Set(accessibleIds);
|
|
return {
|
|
items: result.items.filter((p) => accessible.has(p.id)),
|
|
meta: result.meta,
|
|
};
|
|
}
|
|
|
|
async getLabelInfo(
|
|
name: string,
|
|
type: LabelType,
|
|
workspaceId: string,
|
|
userId: string,
|
|
spaceId?: string,
|
|
) {
|
|
const normalized = normalizeLabelName(name);
|
|
const label = await this.labelRepo.findByNameAndWorkspace(
|
|
normalized,
|
|
workspaceId,
|
|
type,
|
|
);
|
|
|
|
// Uniform response shape.
|
|
// We don't want to expose whether the label row exists
|
|
const usageCount = label
|
|
? await this.labelRepo.getLabelPageCountForUser(
|
|
label.id,
|
|
userId,
|
|
spaceId,
|
|
)
|
|
: 0;
|
|
|
|
return {
|
|
name: normalized,
|
|
usageCount,
|
|
};
|
|
}
|
|
}
|