feat: page labels/tags (#2188)

* feat: labels (WIP)
* full implementation
This commit is contained in:
Philip Okugbe
2026-05-10 18:14:15 +01:00
committed by GitHub
parent 537e45bc11
commit a689cca7a0
32 changed files with 2329 additions and 123 deletions
@@ -40,6 +40,8 @@ import { CreatedByUserDto } from './dto/created-by-user.dto';
import { DuplicatePageDto } from './dto/duplicate-page.dto';
import { DeletedPageDto } from './dto/deleted-page.dto';
import { BacklinksListDto } from './dto/backlink.dto';
import { LabelService } from '../label/label.service';
import { AddLabelsDto, RemoveLabelDto } from '../label/dto/label.dto';
import {
jsonToHtml,
jsonToMarkdown,
@@ -61,6 +63,7 @@ export class PageController {
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pageAccessService: PageAccessService,
private readonly backlinkService: BacklinkService,
private readonly labelService: LabelService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
@@ -99,6 +102,64 @@ export class PageController {
return { ...page, permissions };
}
@HttpCode(HttpStatus.OK)
@Post('labels')
async getPageLabels(
@Body() dto: PageIdDto,
@Body() pagination: PaginationOptions,
@AuthUser() user: User,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
await this.pageAccessService.validateCanView(page, user);
return this.labelService.getPageLabels(page.id, pagination);
}
@HttpCode(HttpStatus.OK)
@Post('labels/add')
async addPageLabels(
@Body() dto: AddLabelsDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page || page.deletedAt) {
throw new NotFoundException('Page not found');
}
await this.pageAccessService.validateCanEdit(page, user);
return this.labelService.addLabelsToPage(
page.id,
dto.names,
workspace.id,
);
}
@HttpCode(HttpStatus.OK)
@Post('labels/remove')
async removePageLabel(
@Body() dto: RemoveLabelDto,
@AuthUser() user: User,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page || page.deletedAt) {
throw new NotFoundException('Page not found');
}
await this.pageAccessService.validateCanEdit(page, user);
await this.labelService.removeLabelFromPage(
page.id,
dto.labelId,
page.workspaceId,
);
}
@HttpCode(HttpStatus.OK)
@Post('backlinks-count')
async getBacklinksCount(
+2
View File
@@ -8,6 +8,7 @@ import { StorageModule } from '../../integrations/storage/storage.module';
import { CollaborationModule } from '../../collaboration/collaboration.module';
import { WatcherModule } from '../watcher/watcher.module';
import { TransclusionModule } from './transclusion/transclusion.module';
import { LabelModule } from '../label/label.module';
@Module({
controllers: [PageController],
@@ -23,6 +24,7 @@ import { TransclusionModule } from './transclusion/transclusion.module';
CollaborationModule,
WatcherModule,
TransclusionModule,
LabelModule,
],
})
export class PageModule {}
@@ -425,11 +425,7 @@ export class PageService {
if (pageIdsToMove.length > 1) {
// Update sub pages (all accessible pages except root)
await this.pageRepo.updatePages(
{ spaceId },
childPageIds,
trx,
);
await this.pageRepo.updatePages({ spaceId }, childPageIds, trx);
}
if (pageIdsToMove.length > 0) {
@@ -476,9 +472,13 @@ export class PageService {
);
// Update watchers and remove those without access to new space
await this.watcherService.movePageWatchersToSpace(pageIdsToMove, spaceId, {
trx,
});
await this.watcherService.movePageWatchersToSpace(
pageIdsToMove,
spaceId,
{
trx,
},
);
await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, {
pageId: pageIdsToMove,
@@ -858,13 +858,15 @@ export class PageService {
.selectFrom('page_ancestors')
.selectAll('page_ancestors')
.select((eb) =>
eb.exists(
eb
.selectFrom('pages as child')
.select(sql`1`.as('one'))
.whereRef('child.parentPageId', '=', 'page_ancestors.id')
.where('child.deletedAt', 'is', null),
).as('hasChildren'),
eb
.exists(
eb
.selectFrom('pages as child')
.select(sql`1`.as('one'))
.whereRef('child.parentPageId', '=', 'page_ancestors.id')
.where('child.deletedAt', 'is', null),
)
.as('hasChildren'),
)
.execute();