From c67eb21ba8cf00eab1806df117eee26eb8d3bbe8 Mon Sep 17 00:00:00 2001 From: johaven Date: Mon, 3 Aug 2026 23:04:13 +0200 Subject: [PATCH 1/6] fix(spaces): display names instead of aliases --- .../interfaces/space-files.interface.ts | 4 +++ .../spaces/services/spaces-browser.service.ts | 7 +++++- .../spaces/services/spaces-browser.service.ts | 25 ++++++++++--------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/backend/src/applications/spaces/interfaces/space-files.interface.ts b/backend/src/applications/spaces/interfaces/space-files.interface.ts index 752f347a1..bf748b549 100644 --- a/backend/src/applications/spaces/interfaces/space-files.interface.ts +++ b/backend/src/applications/spaces/interfaces/space-files.interface.ts @@ -1,6 +1,10 @@ import type { FileProps } from '../../files/interfaces/file-props.interface' export interface SpaceFiles { + space: { + alias: string + name: string + } files: FileProps[] hasRoots: boolean permissions: string diff --git a/backend/src/applications/spaces/services/spaces-browser.service.ts b/backend/src/applications/spaces/services/spaces-browser.service.ts index 6cac9a494..d7105edd4 100644 --- a/backend/src/applications/spaces/services/spaces-browser.service.ts +++ b/backend/src/applications/spaces/services/spaces-browser.service.ts @@ -43,7 +43,12 @@ export class SpacesBrowser { ): Promise { // check sync permission options.withSyncs = options.withSyncs && user.havePermission(USER_PERMISSION.DESKTOP_APP) && user.havePermission(USER_PERMISSION.DESKTOP_APP_SYNC) - const spaceFiles: SpaceFiles = { files: [], hasRoots: false, permissions: space.browsePermissions() } + const spaceFiles: SpaceFiles = { + space: { alias: space.alias, name: space.name }, + files: [], + hasRoots: false, + permissions: space.browsePermissions() + } const [fsFiles, dbFiles, rootFiles] = await Promise.all([ this.parseFS(space), this.parseDB(user.id, space, options), diff --git a/frontend/src/app/applications/spaces/services/spaces-browser.service.ts b/frontend/src/app/applications/spaces/services/spaces-browser.service.ts index 11cb03cc3..f55180144 100644 --- a/frontend/src/app/applications/spaces/services/spaces-browser.service.ts +++ b/frontend/src/app/applications/spaces/services/spaces-browser.service.ts @@ -42,9 +42,9 @@ export class SpacesBrowserService { loadFiles(): Observable { return this.http.get(this.browseApi).pipe( - tap(() => { + tap((spaceFiles) => { this.layout.setBreadcrumbIcon(this.breadCrumbIcon) - this.layout.setBreadcrumbNav(this.breadcrumbNav()) + this.layout.setBreadcrumbNav(this.breadcrumbNav(spaceFiles.space.name)) }), catchError((e: HttpErrorResponse) => { return throwError(() => e) @@ -52,7 +52,16 @@ export class SpacesBrowserService { ) } - private breadcrumbNav(): BreadCrumbUrl { + private breadcrumbNav(spaceName: string): BreadCrumbUrl { + const mutateLevel: NonNullable = {} + if (this.inPersonalSpace && !this.breadCrumbFilesRepo && !this.inRootSpace) { + mutateLevel[0] = { + setTitle: SPACES_TITLE.PERSONAL_FILES, + translateTitle: true + } + } else if (spaceName && !this.inPersonalSpace && !(this.inShareRepo && this.inRootSpace)) { + mutateLevel[0] = { setTitle: spaceName } + } return { url: this.inPersonalSpace && this.inRootSpace @@ -70,15 +79,7 @@ export class SpacesBrowserService { : this.inShareRepo ? SPACES_PATH.SPACES_SHARES : SPACES_PATH.TRASH, - mutateLevel: - this.inPersonalSpace && !this.breadCrumbFilesRepo && !this.inRootSpace - ? { - 0: { - setTitle: SPACES_TITLE.PERSONAL_FILES, - translateTitle: true - } - } - : null, + mutateLevel: Object.keys(mutateLevel).length ? mutateLevel : null, splicing: this.inPersonalSpace ? (!this.breadCrumbFilesRepo && !this.inRootSpace ? 2 : 3) : 2 } satisfies BreadCrumbUrl } From c30752df75c5c334d8232262819799b42afa4750 Mon Sep 17 00:00:00 2001 From: johaven Date: Mon, 3 Aug 2026 23:10:40 +0200 Subject: [PATCH 2/6] fix(recents): display space and share names --- .../comments/interfaces/comment-recent.interface.ts | 2 +- .../comments/services/comments-queries.service.ts | 6 ++++-- .../src/applications/files/schemas/file-recent.interface.ts | 1 + .../applications/files/services/files-queries.service.ts | 5 ++++- .../applications/comments/models/comment-recent.model.ts | 5 +++-- .../src/app/applications/files/models/file-recent.model.ts | 6 +++++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/src/applications/comments/interfaces/comment-recent.interface.ts b/backend/src/applications/comments/interfaces/comment-recent.interface.ts index ee1ede1c8..3a306129f 100644 --- a/backend/src/applications/comments/interfaces/comment-recent.interface.ts +++ b/backend/src/applications/comments/interfaces/comment-recent.interface.ts @@ -5,5 +5,5 @@ export interface CommentRecent { content: string modifiedAt: Date author: Owner - file: { name: string; path: string; mime: string; inTrash: number; fromSpace: number; fromShare: number } + file: { name: string; path: string; mime: string; inTrash: number; fromSpace: number; fromShare: number; displayRootName?: string } } diff --git a/backend/src/applications/comments/services/comments-queries.service.ts b/backend/src/applications/comments/services/comments-queries.service.ts index d9fff60c8..c3ad6c497 100644 --- a/backend/src/applications/comments/services/comments-queries.service.ts +++ b/backend/src/applications/comments/services/comments-queries.service.ts @@ -118,7 +118,8 @@ export class CommentsQueries { mime: files.mime, inTrash: sql`0`.as('inTrash'), fromSpace: sql`0`.as('fromSpace'), - fromShare: sql`1`.as('fromShare') + fromShare: sql`1`.as('fromShare'), + displayRootName: shares.name } } satisfies CommentRecent | SelectedFields) .from(shares) @@ -204,7 +205,8 @@ export class CommentsQueries { mime: files.mime, inTrash: sql`${files.inTrash}`.as('inTrash'), fromSpace: sql`IF (${files.ownerId} = ${userId}, 0, 1)`.as('fromSpace'), - fromShare: sql`0`.as('fromShare') + fromShare: sql`0`.as('fromShare'), + displayRootName: sql`IF (${files.ownerId} = ${userId}, NULL, ${spaces.name})`.as('displayRootName') } } satisfies CommentRecent | SelectedFields) .from(spaces) diff --git a/backend/src/applications/files/schemas/file-recent.interface.ts b/backend/src/applications/files/schemas/file-recent.interface.ts index 984647f92..d0a4c6d49 100644 --- a/backend/src/applications/files/schemas/file-recent.interface.ts +++ b/backend/src/applications/files/schemas/file-recent.interface.ts @@ -12,6 +12,7 @@ export class FileRecent implements FileRecentSchema { name: string mime: string mtime: number + displayRootName?: string } export interface FileRecentLocation { diff --git a/backend/src/applications/files/services/files-queries.service.ts b/backend/src/applications/files/services/files-queries.service.ts index 9cb345025..f84296c28 100644 --- a/backend/src/applications/files/services/files-queries.service.ts +++ b/backend/src/applications/files/services/files-queries.service.ts @@ -297,9 +297,12 @@ export class FilesQueries { path: filesRecents.path, name: filesRecents.name, mime: filesRecents.mime, - mtime: filesRecents.mtime + mtime: filesRecents.mtime, + displayRootName: sql`COALESCE(${spaces.name}, ${shares.name})`.as('displayRootName') } satisfies FileRecent | SelectedFields) .from(filesRecents) + .leftJoin(spaces, eq(spaces.id, filesRecents.spaceId)) + .leftJoin(shares, eq(shares.id, filesRecents.shareId)) .where(or(...where)) .groupBy(filesRecents.id) .orderBy(desc(filesRecents.mtime)) diff --git a/frontend/src/app/applications/comments/models/comment-recent.model.ts b/frontend/src/app/applications/comments/models/comment-recent.model.ts index c03ce5f52..1c1dfa086 100644 --- a/frontend/src/app/applications/comments/models/comment-recent.model.ts +++ b/frontend/src/app/applications/comments/models/comment-recent.model.ts @@ -10,7 +10,7 @@ export class CommentRecentModel implements CommentRecent { content: string modifiedAt: Date author: OwnerType - file: { name: string; path: string; mime: string; inTrash: number; fromSpace: number; fromShare: number } + file: { name: string; path: string; mime: string; inTrash: number; fromSpace: number; fromShare: number; displayRootName?: string } // Computed mimeUrl: string @@ -27,7 +27,8 @@ export class CommentRecentModel implements CommentRecent { this.mimeUrl = getAssetsMimeUrl(this.file.mime) const location = resolveFileLocation(this.file.path, { repository: this.file.fromShare ? 'share' : this.file.fromSpace ? 'space' : 'personal', - appendName: this.file.name + appendName: this.file.name, + displayRootName: this.file.displayRootName }) this.icon = location.icon this.iconClass = location.iconClass diff --git a/frontend/src/app/applications/files/models/file-recent.model.ts b/frontend/src/app/applications/files/models/file-recent.model.ts index d9b5be5f9..8d49369db 100644 --- a/frontend/src/app/applications/files/models/file-recent.model.ts +++ b/frontend/src/app/applications/files/models/file-recent.model.ts @@ -13,6 +13,7 @@ export class FileRecentModel implements FileRecent { path: string shareId: number spaceId: number + displayRootName?: string // Computed mimeUrl: string @@ -24,7 +25,10 @@ export class FileRecentModel implements FileRecent { constructor(props: Partial) { Object.assign(this, props) this.mimeUrl = getAssetsMimeUrl(this.mime) - const location = resolveFileLocation(this.path, { repository: this.shareId ? 'share' : this.spaceId ? 'space' : 'personal' }) + const location = resolveFileLocation(this.path, { + repository: this.shareId ? 'share' : this.spaceId ? 'space' : 'personal', + displayRootName: this.displayRootName + }) this.iconClass = location.iconClass this.icon = location.icon this.showedPath = location.relativePath From bf8cd1adf878ed01b93f644c72467ca76024d593 Mon Sep 17 00:00:00 2001 From: johaven Date: Mon, 3 Aug 2026 23:38:06 +0200 Subject: [PATCH 3/6] fix(files): display space and share names in search results --- .../services/comments-queries.service.ts | 8 ++-- .../files/schemas/file-content.interface.ts | 2 + .../services/files-recents.service.spec.ts | 39 +++++++++--------- .../files/services/files-recents.service.ts | 13 ++++-- .../files-search-manager.service.spec.ts | 40 ++++++++++++++----- .../services/files-search-manager.service.ts | 39 +++++++++++++++--- .../shares/services/shares-queries.service.ts | 21 +++++----- .../spaces/services/spaces-queries.service.ts | 16 +++++--- .../files/models/file-content.model.ts | 3 +- 9 files changed, 123 insertions(+), 58 deletions(-) diff --git a/backend/src/applications/comments/services/comments-queries.service.ts b/backend/src/applications/comments/services/comments-queries.service.ts index c3ad6c497..b89ec4d95 100644 --- a/backend/src/applications/comments/services/comments-queries.service.ts +++ b/backend/src/applications/comments/services/comments-queries.service.ts @@ -238,10 +238,12 @@ export class CommentsQueries { async getRecentsFromUser(user: UserModel, limit = 10): Promise { const hasPersonal = user.havePermission(USER_PERMISSION.PERSONAL_SPACE) - const [spaceIds, shareIds] = await Promise.all([ - user.havePermission(USER_PERMISSION.SPACES) ? this.spacesQueries.spaceIds(user.id) : Promise.resolve([]), - user.havePermission(USER_PERMISSION.SHARES) ? this.sharesQueries.shareIds(user.id, +user.isAdmin) : Promise.resolve([]) + const [spaces, shares] = await Promise.all([ + user.havePermission(USER_PERMISSION.SPACES) ? this.spacesQueries.spaceIdentities(user.id) : Promise.resolve([]), + user.havePermission(USER_PERMISSION.SHARES) ? this.sharesQueries.shareIdentities(user.id, +user.isAdmin) : Promise.resolve([]) ]) + const spaceIds = spaces.map(({ id }) => id) + const shareIds = shares.map(({ id }) => id) const hasSpaces = spaceIds.length > 0 const hasShares = shareIds.length > 0 const sourceCount = +hasPersonal + +hasSpaces + +hasShares diff --git a/backend/src/applications/files/schemas/file-content.interface.ts b/backend/src/applications/files/schemas/file-content.interface.ts index 12695f933..7c28a5656 100644 --- a/backend/src/applications/files/schemas/file-content.interface.ts +++ b/backend/src/applications/files/schemas/file-content.interface.ts @@ -11,6 +11,8 @@ export interface FileContent { matches?: string[] // used for search score?: number + // used for search display + displayRootName?: string } export type FileContentRecordMetadata = Pick diff --git a/backend/src/applications/files/services/files-recents.service.spec.ts b/backend/src/applications/files/services/files-recents.service.spec.ts index d94b30be2..14c3a32ee 100644 --- a/backend/src/applications/files/services/files-recents.service.spec.ts +++ b/backend/src/applications/files/services/files-recents.service.spec.ts @@ -21,10 +21,10 @@ describe(FilesRecents.name, () => { upsertRecent: Mock } let spacesQueries: { - spaceIds: Mock + spaceIdentities: Mock } let sharesQueries: { - shareIds: Mock + shareIdentities: Mock } beforeEach(async () => { @@ -38,10 +38,10 @@ describe(FilesRecents.name, () => { upsertRecent: vi.fn().mockResolvedValue(undefined) } spacesQueries = { - spaceIds: vi.fn().mockResolvedValue([]) + spaceIdentities: vi.fn().mockResolvedValue([]) } sharesQueries = { - shareIds: vi.fn().mockResolvedValue([]) + shareIdentities: vi.fn().mockResolvedValue([]) } const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -74,8 +74,11 @@ describe(FilesRecents.name, () => { it('should load recents from user accessible spaces and shares', async () => { const recents = [{ id: 1, name: 'a.txt' }] - spacesQueries.spaceIds.mockResolvedValueOnce([10, 11]) - sharesQueries.shareIds.mockResolvedValueOnce([20]) + spacesQueries.spaceIdentities.mockResolvedValueOnce([ + { id: 10, alias: 'first', name: 'First' }, + { id: 11, alias: 'second', name: 'Second' } + ]) + sharesQueries.shareIdentities.mockResolvedValueOnce([{ id: 20, alias: 'shared', name: 'Shared' }]) filesQueries.getRecentsFromUser.mockResolvedValueOnce(recents) const result = await service.getRecents( @@ -83,8 +86,8 @@ describe(FilesRecents.name, () => { 25 ) - expect(spacesQueries.spaceIds).toHaveBeenCalledWith(7) - expect(sharesQueries.shareIds).toHaveBeenCalledWith(7, 1) + expect(spacesQueries.spaceIdentities).toHaveBeenCalledWith(7) + expect(sharesQueries.shareIdentities).toHaveBeenCalledWith(7, 1) expect(filesQueries.getRecentsFromUser).toHaveBeenCalledWith(7, [10, 11], [20], 25) expect(result).toBe(recents) }) @@ -92,36 +95,36 @@ describe(FilesRecents.name, () => { it('should only load personal recents when user only has personal space permission', async () => { await service.getRecents(userWithPermissions([USER_PERMISSION.PERSONAL_SPACE]), 10) - expect(spacesQueries.spaceIds).not.toHaveBeenCalled() - expect(sharesQueries.shareIds).not.toHaveBeenCalled() + expect(spacesQueries.spaceIdentities).not.toHaveBeenCalled() + expect(sharesQueries.shareIdentities).not.toHaveBeenCalled() expect(filesQueries.getRecentsFromUser).toHaveBeenCalledWith(7, [], [], 10) }) it('should only load space recents when user only has spaces permission', async () => { - spacesQueries.spaceIds.mockResolvedValueOnce([10]) + spacesQueries.spaceIdentities.mockResolvedValueOnce([{ id: 10, alias: 'space', name: 'Space' }]) await service.getRecents(userWithPermissions([USER_PERMISSION.SPACES]), 10) - expect(spacesQueries.spaceIds).toHaveBeenCalledWith(7) - expect(sharesQueries.shareIds).not.toHaveBeenCalled() + expect(spacesQueries.spaceIdentities).toHaveBeenCalledWith(7) + expect(sharesQueries.shareIdentities).not.toHaveBeenCalled() expect(filesQueries.getRecentsFromUser).toHaveBeenCalledWith(undefined, [10], [], 10) }) it('should only load share recents when user only has shares permission', async () => { - sharesQueries.shareIds.mockResolvedValueOnce([20]) + sharesQueries.shareIdentities.mockResolvedValueOnce([{ id: 20, alias: 'share', name: 'Share' }]) await service.getRecents(userWithPermissions([USER_PERMISSION.SHARES]), 10) - expect(spacesQueries.spaceIds).not.toHaveBeenCalled() - expect(sharesQueries.shareIds).toHaveBeenCalledWith(7, 0) + expect(spacesQueries.spaceIdentities).not.toHaveBeenCalled() + expect(sharesQueries.shareIdentities).toHaveBeenCalledWith(7, 0) expect(filesQueries.getRecentsFromUser).toHaveBeenCalledWith(undefined, [], [20], 10) }) it('should not load any recents source without matching permission', async () => { await service.getRecents(userWithPermissions(), 10) - expect(spacesQueries.spaceIds).not.toHaveBeenCalled() - expect(sharesQueries.shareIds).not.toHaveBeenCalled() + expect(spacesQueries.spaceIdentities).not.toHaveBeenCalled() + expect(sharesQueries.shareIdentities).not.toHaveBeenCalled() expect(filesQueries.getRecentsFromUser).toHaveBeenCalledWith(undefined, [], [], 10) }) diff --git a/backend/src/applications/files/services/files-recents.service.ts b/backend/src/applications/files/services/files-recents.service.ts index 534696e36..0f55372ab 100644 --- a/backend/src/applications/files/services/files-recents.service.ts +++ b/backend/src/applications/files/services/files-recents.service.ts @@ -23,12 +23,17 @@ export class FilesRecents { ) {} async getRecents(user: UserModel, limit: number): Promise { - const [spaceIds, shareIds] = await Promise.all([ - user.havePermission(USER_PERMISSION.SPACES) ? this.spacesQueries.spaceIds(user.id) : Promise.resolve([]), - user.havePermission(USER_PERMISSION.SHARES) ? this.sharesQueries.shareIds(user.id, +user.isAdmin) : Promise.resolve([]) + const [spaces, shares] = await Promise.all([ + user.havePermission(USER_PERMISSION.SPACES) ? this.spacesQueries.spaceIdentities(user.id) : Promise.resolve([]), + user.havePermission(USER_PERMISSION.SHARES) ? this.sharesQueries.shareIdentities(user.id, +user.isAdmin) : Promise.resolve([]) ]) const ownerId = user.havePermission(USER_PERMISSION.PERSONAL_SPACE) ? user.id : undefined - return this.filesQueries.getRecentsFromUser(ownerId, spaceIds, shareIds, limit) + return this.filesQueries.getRecentsFromUser( + ownerId, + spaces.map(({ id }) => id), + shares.map(({ id }) => id), + limit + ) } async updateRecents(user: UserModel, space: SpaceEnv, files: FileProps[]): Promise { diff --git a/backend/src/applications/files/services/files-search-manager.service.spec.ts b/backend/src/applications/files/services/files-search-manager.service.spec.ts index 309ec9167..abae101ae 100644 --- a/backend/src/applications/files/services/files-search-manager.service.spec.ts +++ b/backend/src/applications/files/services/files-search-manager.service.spec.ts @@ -20,10 +20,10 @@ describe(FilesSearchManager.name, () => { allPaths: Mock } let spacesQueries: { - spaceIds: Mock + spaceIdentities: Mock } let sharesQueries: { - shareIds: Mock + shareIdentities: Mock } let contentIndexingEnabled: boolean @@ -45,10 +45,10 @@ describe(FilesSearchManager.name, () => { allPaths: vi.fn() } spacesQueries = { - spaceIds: vi.fn().mockResolvedValue([]) + spaceIdentities: vi.fn().mockResolvedValue([]) } sharesQueries = { - shareIds: vi.fn().mockResolvedValue([]) + shareIdentities: vi.fn().mockResolvedValue([]) } const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -88,21 +88,27 @@ describe(FilesSearchManager.name, () => { it('should route to full-text search with space and share ids', async () => { configuration.applications.files.contentIndexing.enabled = true - spacesQueries.spaceIds.mockResolvedValueOnce([1, 2]) - sharesQueries.shareIds.mockResolvedValueOnce([4]) + spacesQueries.spaceIdentities.mockResolvedValueOnce([ + { id: 1, alias: 'first', name: 'First' }, + { id: 2, alias: 'second', name: 'Second' } + ]) + sharesQueries.shareIdentities.mockResolvedValueOnce([{ id: 4, alias: 'shared', name: 'Shared' }]) const fullTextSpy = vi.spyOn(service as any, 'searchFullText').mockResolvedValueOnce([fileContent('match.md')]) const result = await service.search({ id: 10, isAdmin: true } as any, { content: 'match', fullText: true, limit: 5 } as any) - expect(spacesQueries.spaceIds).toHaveBeenCalledWith(10) - expect(sharesQueries.shareIds).toHaveBeenCalledWith(10, 1) + expect(spacesQueries.spaceIdentities).toHaveBeenCalledWith(10) + expect(sharesQueries.shareIdentities).toHaveBeenCalledWith(10, 1) expect(fullTextSpy).toHaveBeenCalledWith(10, [1, 2], [4], 'match', 5) expect(result).toEqual([fileContent('match.md')]) }) it('should route to filename search when fullText is false', async () => { - spacesQueries.spaceIds.mockResolvedValueOnce([6]) - sharesQueries.shareIds.mockResolvedValueOnce([8, 9]) + spacesQueries.spaceIdentities.mockResolvedValueOnce([{ id: 6, alias: 'reports', name: 'Reports' }]) + sharesQueries.shareIdentities.mockResolvedValueOnce([ + { id: 8, alias: 'first-share', name: 'First share' }, + { id: 9, alias: 'second-share', name: 'Second share' } + ]) const nameSearchSpy = vi.spyOn(service as any, 'searchFileNames').mockResolvedValueOnce([fileContent('report.pdf')]) const result = await service.search({ id: 3, isAdmin: false } as any, { content: 'report', fullText: false, limit: 2 } as any) @@ -111,6 +117,20 @@ describe(FilesSearchManager.name, () => { expect(result).toEqual([fileContent('report.pdf')]) }) + it('adds current space and share names to search results', async () => { + spacesQueries.spaceIdentities.mockResolvedValueOnce([{ id: 6, alias: 'communication', name: 'Communication' }]) + sharesQueries.shareIdentities.mockResolvedValueOnce([{ id: 8, alias: 'public-access', name: 'Public access' }]) + vi.spyOn(service as any, 'searchFileNames').mockResolvedValueOnce([ + { ...fileContent('space.md'), path: 'files/communication/docs' }, + { ...fileContent('share.md'), path: 'shares/public-access/docs' }, + { ...fileContent('personal.md'), path: 'files/personal/docs' } + ]) + + const result = await service.search({ id: 3, isAdmin: false } as any, { content: 'docs', fullText: false, limit: 3 } as any) + + expect(result.map(({ displayRootName }) => displayRootName)).toEqual(['Communication', 'Public access', undefined]) + }) + it('should normalize search limit before routing', async () => { const nameSearchSpy = vi.spyOn(service as any, 'searchFileNames').mockResolvedValue([]) diff --git a/backend/src/applications/files/services/files-search-manager.service.ts b/backend/src/applications/files/services/files-search-manager.service.ts index c605c4dac..616d10442 100644 --- a/backend/src/applications/files/services/files-search-manager.service.ts +++ b/backend/src/applications/files/services/files-search-manager.service.ts @@ -4,6 +4,7 @@ import { Stats } from 'node:fs' import path from 'node:path' import { configuration } from '../../../configuration/config.environment' import { SharesQueries } from '../../shares/services/shares-queries.service' +import { SPACE_REPOSITORY } from '../../spaces/constants/spaces' import { SpacesQueries } from '../../spaces/services/spaces-queries.service' import { UserModel } from '../../users/models/user.model' import { SearchFilesDto } from '../dto/file-operations.dto' @@ -32,12 +33,16 @@ export class FilesSearchManager { throw new HttpException('Full-text search is disabled', HttpStatus.BAD_REQUEST) } const limit = normalizeSearchLimit(search.limit) - const [spaceIds, shareIds] = await Promise.all([this.spacesQueries.spaceIds(user.id), this.sharesQueries.shareIds(user.id, +user.isAdmin)]) - if (search.fullText) { - return await this.searchFullText(user.id, spaceIds, shareIds, search.content, limit) - } else { - return await this.searchFileNames(user.id, spaceIds, shareIds, search.content, limit) - } + const [userSpaces, userShares] = await Promise.all([ + this.spacesQueries.spaceIdentities(user.id), + this.sharesQueries.shareIdentities(user.id, +user.isAdmin) + ]) + const spaceIds = userSpaces.map((space) => space.id) + const shareIds = userShares.map((share) => share.id) + const fileContents = await (search.fullText + ? this.searchFullText(user.id, spaceIds, shareIds, search.content, limit) + : this.searchFileNames(user.id, spaceIds, shareIds, search.content, limit)) + return this.setDisplayRootNames(fileContents, userSpaces, userShares) } private async searchFullText(userId: number, spaceIds: number[], shareIds: number[], search: string, limit: number): Promise { @@ -73,6 +78,7 @@ export class FilesSearchManager { const f = await this.analyzeFile(p.realPath, p.pathPrefix, regexBasePath, regexpTerms) if (f !== null) { fileContents.push(f) + if (fileContents.length >= limit) return fileContents } continue } @@ -117,4 +123,25 @@ export class FilesSearchManager { mtime: stats.mtime.getTime() } } + + private setDisplayRootNames( + fileContents: FileContent[], + userSpaces: { alias?: string; name?: string }[], + userShares: { alias?: string; name?: string }[] + ): FileContent[] { + if (fileContents.length === 0) return fileContents + const displayRootNames = new Map() + for (const { alias, name } of userSpaces) { + if (alias && name) displayRootNames.set(`${SPACE_REPOSITORY.FILES}/${alias}`, name) + } + for (const { alias, name } of userShares) { + if (alias && name) displayRootNames.set(`${SPACE_REPOSITORY.SHARES}/${alias}`, name) + } + for (const fileContent of fileContents) { + const [repository, alias] = fileContent.path.split('/', 2) + const displayRootName = displayRootNames.get(`${repository}/${alias}`) + if (displayRootName) fileContent.displayRootName = displayRootName + } + return fileContents + } } diff --git a/backend/src/applications/shares/services/shares-queries.service.ts b/backend/src/applications/shares/services/shares-queries.service.ts index cc236b5db..99b6875ec 100644 --- a/backend/src/applications/shares/services/shares-queries.service.ts +++ b/backend/src/applications/shares/services/shares-queries.service.ts @@ -52,7 +52,7 @@ import { shares } from '../schemas/shares.schema' export class SharesQueries { private readonly logger = new Logger(SharesQueries.name) private sharesListQuery: MySql2PreparedQuery = null - private shareIdsQuery: MySql2PreparedQuery = null + private shareIdentitiesQuery: MySql2PreparedQuery = null private shareLinksListQuery: MySql2PreparedQuery = null private shareWithMembersQuery: MySql2PreparedQuery = null private sharePermissionsQuery: MySql2PreparedQuery = null @@ -473,16 +473,17 @@ export class SharesQueries { } @CacheDecorator() - async shareIds(userId: number, isAdmin: number): Promise { - if (!this.shareIdsQuery) { - const unionAlias = union( - this.fromUserQuery({ id: shares.id }), - this.fromGroupsQuery({ id: shares.id }), - this.fromAdminSharesQuery({ id: shares.id }) - ).as('unionAlias') - this.shareIdsQuery = this.db.select({ id: unionAlias.id }).from(unionAlias).groupBy(unionAlias.id).prepare() + async shareIdentities(userId: number, isAdmin: number): Promise[]> { + if (!this.shareIdentitiesQuery) { + const select = { id: shares.id, alias: shares.alias, name: shares.name } + const unionAlias = union(this.fromUserQuery(select), this.fromGroupsQuery(select), this.fromAdminSharesQuery(select)).as('unionAlias') + this.shareIdentitiesQuery = this.db + .select({ id: unionAlias.id, alias: unionAlias.alias, name: unionAlias.name }) + .from(unionAlias) + .groupBy(unionAlias.id, unionAlias.alias, unionAlias.name) + .prepare() } - return (await this.shareIdsQuery.execute({ userId: userId, isAdmin: +isAdmin })).map((r: { id: number }) => r.id) + return this.shareIdentitiesQuery.execute({ userId: userId, isAdmin: +isAdmin }) } async listShares(user: UserModel): Promise { diff --git a/backend/src/applications/spaces/services/spaces-queries.service.ts b/backend/src/applications/spaces/services/spaces-queries.service.ts index 04d2b1fb2..5231c663a 100644 --- a/backend/src/applications/spaces/services/spaces-queries.service.ts +++ b/backend/src/applications/spaces/services/spaces-queries.service.ts @@ -54,7 +54,7 @@ export class SpacesQueries { private spaceAndRootPermissionsQuery: MySql2PreparedQuery = null private spacesWithDetailsQuery: MySql2PreparedQuery = null private spacesQuery: MySql2PreparedQuery = null - private spaceIdsQuery: MySql2PreparedQuery = null + private spaceIdentitiesQuery: MySql2PreparedQuery = null private spaceQuery: MySql2PreparedQuery = null private spacesWithPermissionsQuery: MySql2PreparedQuery = null private spaceFromIdWithPermissionsQuery: MySql2PreparedQuery = null @@ -476,13 +476,17 @@ export class SpacesQueries { } @CacheDecorator() - async spaceIds(userId: number): Promise { - if (!this.spaceIdsQuery) { - const unionAlias = this.fromUserAndGroups({ id: spaces.id }) - this.spaceIdsQuery = this.db.select({ id: unionAlias.id }).from(unionAlias).groupBy(unionAlias.id).prepare() + async spaceIdentities(userId: number): Promise[]> { + if (!this.spaceIdentitiesQuery) { + const unionAlias = this.fromUserAndGroups({ id: spaces.id, alias: spaces.alias, name: spaces.name }) + this.spaceIdentitiesQuery = this.db + .select({ id: unionAlias.id, alias: unionAlias.alias, name: unionAlias.name }) + .from(unionAlias) + .groupBy(unionAlias.id, unionAlias.alias, unionAlias.name) + .prepare() } // `userId` is used in `fromUserAndGroups` function - return (await this.spaceIdsQuery.execute({ userId })).map((r: { id: number }) => r.id) + return this.spaceIdentitiesQuery.execute({ userId }) } @CacheDecorator() diff --git a/frontend/src/app/applications/files/models/file-content.model.ts b/frontend/src/app/applications/files/models/file-content.model.ts index b4e37788e..05ec88052 100644 --- a/frontend/src/app/applications/files/models/file-content.model.ts +++ b/frontend/src/app/applications/files/models/file-content.model.ts @@ -11,6 +11,7 @@ export class FileContentModel implements FileContent { size: number mtime: number matches: string[] + displayRootName?: string // Computed mimeUrl: string @@ -29,7 +30,7 @@ export class FileContentModel implements FileContent { } private setProperties() { - const location = resolveFileLocation(this.path) + const location = resolveFileLocation(this.path, { displayRootName: this.displayRootName }) if (!location) return this.showedPath = location.relativePath this.iconClass = location.iconClass From da6dc1eecf0a348efec9f302b49bf7f5f0870ea5 Mon Sep 17 00:00:00 2001 From: johaven Date: Mon, 3 Aug 2026 23:45:59 +0200 Subject: [PATCH 4/6] fix(backend:files): invalidate space and share identities --- .../shares/services/shares-queries.service.ts | 15 ++++++++++----- .../spaces/services/spaces-queries.service.ts | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/backend/src/applications/shares/services/shares-queries.service.ts b/backend/src/applications/shares/services/shares-queries.service.ts index 99b6875ec..f564c0ccb 100644 --- a/backend/src/applications/shares/services/shares-queries.service.ts +++ b/backend/src/applications/shares/services/shares-queries.service.ts @@ -902,11 +902,16 @@ export class SharesQueries { async clearCachePermissions(shareAlias: string, userIds: number[]) { // `permissions` argument must match with `this.permissions.name` function for (const userId of userIds) { - const pattern = this.cache.genSlugKey(this.constructor.name, this.permissions.name, userId, shareAlias, '*') - const keys = await this.cache.keys(pattern) - if (keys.length) { - this.logger.verbose({ tag: this.clearCachePermissions.name, msg: `${JSON.stringify(keys)}` }) - this.cache.mdel(keys).catch((e: Error) => this.logger.error({ tag: this.clearCachePermissions.name, msg: `${e}` })) + const patterns = [ + this.cache.genSlugKey(this.constructor.name, this.permissions.name, userId, shareAlias, '*'), + this.cache.genSlugKey(this.constructor.name, this.shareIdentities.name, userId, '*') + ] + for (const pattern of patterns) { + const keys = await this.cache.keys(pattern) + if (keys.length) { + this.logger.verbose({ tag: this.clearCachePermissions.name, msg: `${JSON.stringify(keys)}` }) + this.cache.mdel(keys).catch((e: Error) => this.logger.error({ tag: this.clearCachePermissions.name, msg: `${e}` })) + } } } } diff --git a/backend/src/applications/spaces/services/spaces-queries.service.ts b/backend/src/applications/spaces/services/spaces-queries.service.ts index 5231c663a..326fb411d 100644 --- a/backend/src/applications/spaces/services/spaces-queries.service.ts +++ b/backend/src/applications/spaces/services/spaces-queries.service.ts @@ -716,6 +716,8 @@ export class SpacesQueries { // clear cache on space root rootAliases.forEach((rAlias: string) => patterns.push(this.cache.genSlugKey(...basePattern, rAlias))) } else { + // clear cache on accessible space identities + patterns.push(this.cache.genSlugKey(this.constructor.name, this.spaceIdentities.name, uid)) // clear cache on spaces list patterns.push(this.cache.genSlugKey(...[this.constructor.name, this.spaces.name, uid])) patterns.push(this.cache.genSlugKey(...[this.constructor.name, this.spaces.name, uid, '*'])) From 90b39fd11eec6cd84e32128ca907ee2ebaa9c043 Mon Sep 17 00:00:00 2001 From: johaven Date: Tue, 4 Aug 2026 00:14:29 +0200 Subject: [PATCH 5/6] fix(frontend:files): improve clipboard path presentation --- .../comments/models/comment-recent.model.ts | 4 +++- .../sidebar/files-clipboard.component.html | 8 ++++---- .../sidebar/files-clipboard.component.ts | 17 ++++++++++++++++- .../components/sidebar/files-tree.component.ts | 4 ++-- .../components/utils/file-location.utils.ts | 17 +++++++++-------- .../files/models/file-recent.model.ts | 5 +++-- .../applications/sync/models/sync-path.model.ts | 3 ++- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/applications/comments/models/comment-recent.model.ts b/frontend/src/app/applications/comments/models/comment-recent.model.ts index 1c1dfa086..c840eaf6e 100644 --- a/frontend/src/app/applications/comments/models/comment-recent.model.ts +++ b/frontend/src/app/applications/comments/models/comment-recent.model.ts @@ -1,5 +1,7 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import type { CommentRecent } from '@sync-in-server/backend/src/applications/comments/interfaces/comment-recent.interface' +import { FILE_REPOSITORY } from '@sync-in-server/backend/src/applications/files/constants/operations' +import { SPACE_ALIAS } from '@sync-in-server/backend/src/applications/spaces/constants/spaces' import { resolveFileLocation } from '../../files/components/utils/file-location.utils' import { getAssetsMimeUrl } from '../../files/files.constants' import { OwnerType } from '../../users/interfaces/owner.interface' @@ -26,7 +28,7 @@ export class CommentRecentModel implements CommentRecent { } this.mimeUrl = getAssetsMimeUrl(this.file.mime) const location = resolveFileLocation(this.file.path, { - repository: this.file.fromShare ? 'share' : this.file.fromSpace ? 'space' : 'personal', + repository: this.file.fromShare ? FILE_REPOSITORY.SHARE : this.file.fromSpace ? FILE_REPOSITORY.SPACE : SPACE_ALIAS.PERSONAL, appendName: this.file.name, displayRootName: this.file.displayRootName }) diff --git a/frontend/src/app/applications/files/components/sidebar/files-clipboard.component.html b/frontend/src/app/applications/files/components/sidebar/files-clipboard.component.html index c5ff61889..1841e50f1 100644 --- a/frontend/src/app/applications/files/components/sidebar/files-clipboard.component.html +++ b/frontend/src/app/applications/files/components/sidebar/files-clipboard.component.html @@ -17,20 +17,20 @@ @if (files.length) {
-
+
- +
-
+