From f6ef19e61f3d02f7ef96d671bb96ed254a0f317c Mon Sep 17 00:00:00 2001 From: cristianoliveira Date: Fri, 17 Jul 2026 17:03:30 +0200 Subject: [PATCH 1/3] fix: return OR results when multiple tags are selected in Drive filter [WPB-27126] Multi-tag search in the Cells API previously collapsed all selected tags into a single JSON-encoded term (e.g. "sam,bug"), which the backend treated as one literal tag value that no node had, so results came back empty. Emit one Metadata entry per tag with Operation 'Should' and a raw term, mirroring the iOS client (WireMessaging RestAPI.swift) and the existing mimeTypes/creatorIds pattern in the same file. This restores OR semantics: a file matching any selected tag now appears in results. Single-tag search is unaffected. Verified end to end against the fulu backend: two tags on distinct files return both files. --- .../api-client/src/cells/cellsApi.test.ts | 36 ++++++++++++++++--- libraries/api-client/src/cells/cellsApi.ts | 8 +++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/libraries/api-client/src/cells/cellsApi.test.ts b/libraries/api-client/src/cells/cellsApi.test.ts index c6966dd50cf..cbe40d1ddf5 100644 --- a/libraries/api-client/src/cells/cellsApi.test.ts +++ b/libraries/api-client/src/cells/cellsApi.test.ts @@ -1914,9 +1914,9 @@ describe('CellsAPI', () => { ); }); - it('filters by tags when provided', async () => { + it('filters by a single tag with Should operation', async () => { const searchPhrase = 'test'; - const tags = ['tag1', 'tag2']; + const tags = ['tag1']; const mockResponse: RestNodeCollection = { Nodes: [ { @@ -1938,7 +1938,7 @@ describe('CellsAPI', () => { Status: { Deleted: 'Not', }, - Metadata: [{Namespace: 'usermeta-tags', Term: JSON.stringify(tags.join(','))}], + Metadata: [{Namespace: 'usermeta-tags', Term: 'tag1', Operation: 'Should'}], }, Flags: ['WithPreSignedURLs'], Limit: '10', @@ -1947,6 +1947,34 @@ describe('CellsAPI', () => { expect(result).toEqual(mockResponse); }); + it('filters by multiple tags with Should operation (OR) so any selected tag matches (WPB-27126)', async () => { + const searchPhrase = 'test'; + const tags = ['tag1', 'tag2']; + const mockResponse: RestNodeCollection = {Nodes: []} as RestNodeCollection; + + mockNodeServiceApi.lookup.mockResolvedValueOnce(createMockResponse(mockResponse)); + + await cellsAPI.searchNodes({phrase: searchPhrase, tags}); + + expect(mockNodeServiceApi.lookup).toHaveBeenCalledWith({ + Scope: {Root: {Path: '/'}, Recursive: true}, + Filters: { + Text: {SearchIn: 'BaseName', Term: searchPhrase}, + Type: 'UNKNOWN', + Status: { + Deleted: 'Not', + }, + Metadata: [ + {Namespace: 'usermeta-tags', Term: 'tag1', Operation: 'Should'}, + {Namespace: 'usermeta-tags', Term: 'tag2', Operation: 'Should'}, + ], + }, + Flags: ['WithPreSignedURLs'], + Limit: '10', + Offset: '0', + }); + }); + it('handles empty tags array', async () => { const searchPhrase = 'test'; const tags: string[] = []; @@ -2249,7 +2277,7 @@ describe('CellsAPI', () => { Type: 'UNKNOWN', Status: {Deleted: 'Not', HasPublicLink: true}, Metadata: [ - {Namespace: 'usermeta-tags', Term: JSON.stringify(tags.join(','))}, + {Namespace: 'usermeta-tags', Term: 'important', Operation: 'Should'}, {Namespace: 'mime', Term: 'image/*', Operation: 'Must'}, {Namespace: 'usermeta-owner-uuid', Term: JSON.stringify(creatorId), Operation: 'Must'}, ], diff --git a/libraries/api-client/src/cells/cellsApi.ts b/libraries/api-client/src/cells/cellsApi.ts index c1c3268cd41..b55fa256a39 100644 --- a/libraries/api-client/src/cells/cellsApi.ts +++ b/libraries/api-client/src/cells/cellsApi.ts @@ -468,9 +468,11 @@ export class CellsAPI { ...(hasPublicLink !== undefined ? {HasPublicLink: hasPublicLink} : {}), }, Metadata: [ - ...(tags !== undefined && tags.length > 0 - ? [{Namespace: USER_META_TAGS_NAMESPACE, Term: this.transformTagsToJson(tags)}] - : []), + ...(tags?.map(tag => ({ + Namespace: USER_META_TAGS_NAMESPACE, + Term: tag, + Operation: 'Should' as const, + })) ?? []), ...(mimeTypes?.map(term => ({Namespace: MIME_NAMESPACE, Term: term, Operation: mimeOp})) ?? []), ...(creatorIds?.map(term => ({ Namespace: USER_META_OWNER_UUID_NAMESPACE, From edffa17b89dc80f1d57b06a8d83f9cf9cc4644e7 Mon Sep 17 00:00:00 2001 From: cristianoliveira Date: Fri, 17 Jul 2026 18:33:43 +0200 Subject: [PATCH 2/3] refactor: extract createTagMetadataFilters and tighten test name Address review on PR #21898: - Extract tag metadata filter construction into a named module-level helper with an explicit LookupFilterMetaFilter[] return type, dropping the inline as const. - Rename the multi-tag test to describe what it actually verifies (request shape, not backend OR semantics). Mime/creator filter extraction is deferred to a follow-up so this fix stays scoped to WPB-27126. --- .../api-client/src/cells/cellsApi.test.ts | 2 +- libraries/api-client/src/cells/cellsApi.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/libraries/api-client/src/cells/cellsApi.test.ts b/libraries/api-client/src/cells/cellsApi.test.ts index cbe40d1ddf5..b7b83648fac 100644 --- a/libraries/api-client/src/cells/cellsApi.test.ts +++ b/libraries/api-client/src/cells/cellsApi.test.ts @@ -1947,7 +1947,7 @@ describe('CellsAPI', () => { expect(result).toEqual(mockResponse); }); - it('filters by multiple tags with Should operation (OR) so any selected tag matches (WPB-27126)', async () => { + it('creates one Should metadata filter per selected tag', async () => { const searchPhrase = 'test'; const tags = ['tag1', 'tag2']; const mockResponse: RestNodeCollection = {Nodes: []} as RestNodeCollection; diff --git a/libraries/api-client/src/cells/cellsApi.ts b/libraries/api-client/src/cells/cellsApi.ts index b55fa256a39..4a749a3aaee 100644 --- a/libraries/api-client/src/cells/cellsApi.ts +++ b/libraries/api-client/src/cells/cellsApi.ts @@ -20,6 +20,7 @@ import {AxiosHeaders} from 'axios'; import { NodeServiceApi, + LookupFilterMetaFilter, RestLookupRequest, RestCreateCheckResponse, RestDeleteVersionResponse, @@ -55,6 +56,17 @@ const USER_META_TAGS_NAMESPACE = 'usermeta-tags'; const USER_META_OWNER_UUID_NAMESPACE = 'usermeta-owner-uuid'; const MIME_NAMESPACE = 'mime'; +// Each selected tag is sent as its own metadata filter with the `Should` operation so the +// backend applies OR semantics across tags (a node matching any selected tag is returned). +// Matches the iOS client shape in WireMessaging RestAPI.swift. +function createTagMetadataFilters(tags: string[] | undefined): LookupFilterMetaFilter[] { + return (tags ?? []).map(tag => ({ + Namespace: USER_META_TAGS_NAMESPACE, + Term: tag, + Operation: 'Should', + })); +} + // TODO: remove the apiKey (from pydio and s3) once the Pydio backend has fully support for the auth with the Wire's access token // If it's passed we use it to authenticate, instead of the access token interface CellsConfig { @@ -468,11 +480,7 @@ export class CellsAPI { ...(hasPublicLink !== undefined ? {HasPublicLink: hasPublicLink} : {}), }, Metadata: [ - ...(tags?.map(tag => ({ - Namespace: USER_META_TAGS_NAMESPACE, - Term: tag, - Operation: 'Should' as const, - })) ?? []), + ...createTagMetadataFilters(tags), ...(mimeTypes?.map(term => ({Namespace: MIME_NAMESPACE, Term: term, Operation: mimeOp})) ?? []), ...(creatorIds?.map(term => ({ Namespace: USER_META_OWNER_UUID_NAMESPACE, From 008e554489c7c7b4fd656c773b8d8e460534e53f Mon Sep 17 00:00:00 2001 From: cristianoliveira Date: Tue, 21 Jul 2026 13:47:00 +0200 Subject: [PATCH 3/3] fix: address cells tag filter review Keep tag metadata filter construction focused on transforming normalized tag arrays. Rename request-shape tests to describe what they assert and align the default search path expectation with CellsAPI defaults. --- libraries/api-client/src/cells/cellsApi.test.ts | 4 ++-- libraries/api-client/src/cells/cellsApi.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/api-client/src/cells/cellsApi.test.ts b/libraries/api-client/src/cells/cellsApi.test.ts index b7b83648fac..25a9455a2b7 100644 --- a/libraries/api-client/src/cells/cellsApi.test.ts +++ b/libraries/api-client/src/cells/cellsApi.test.ts @@ -1914,7 +1914,7 @@ describe('CellsAPI', () => { ); }); - it('filters by a single tag with Should operation', async () => { + it('creates a Should metadata filter for a single tag', async () => { const searchPhrase = 'test'; const tags = ['tag1']; const mockResponse: RestNodeCollection = { @@ -1957,7 +1957,7 @@ describe('CellsAPI', () => { await cellsAPI.searchNodes({phrase: searchPhrase, tags}); expect(mockNodeServiceApi.lookup).toHaveBeenCalledWith({ - Scope: {Root: {Path: '/'}, Recursive: true}, + Scope: {Root: {Path: ''}, Recursive: true}, Filters: { Text: {SearchIn: 'BaseName', Term: searchPhrase}, Type: 'UNKNOWN', diff --git a/libraries/api-client/src/cells/cellsApi.ts b/libraries/api-client/src/cells/cellsApi.ts index 4a749a3aaee..648df68d802 100644 --- a/libraries/api-client/src/cells/cellsApi.ts +++ b/libraries/api-client/src/cells/cellsApi.ts @@ -59,8 +59,8 @@ const MIME_NAMESPACE = 'mime'; // Each selected tag is sent as its own metadata filter with the `Should` operation so the // backend applies OR semantics across tags (a node matching any selected tag is returned). // Matches the iOS client shape in WireMessaging RestAPI.swift. -function createTagMetadataFilters(tags: string[] | undefined): LookupFilterMetaFilter[] { - return (tags ?? []).map(tag => ({ +function createTagMetadataFilters(tags: string[]): LookupFilterMetaFilter[] { + return tags.map(tag => ({ Namespace: USER_META_TAGS_NAMESPACE, Term: tag, Operation: 'Should', @@ -462,6 +462,7 @@ export class CellsAPI { throw new Error(CONFIGURATION_ERROR); } + const tagMetadataFilters = createTagMetadataFilters(tags ?? []); const mimeOp: 'Should' | 'Must' = mimeTypes !== undefined && mimeTypes.length > 1 ? 'Should' : 'Must'; const creatorOp: 'Should' | 'Must' = creatorIds !== undefined && creatorIds.length > 1 ? 'Should' : 'Must'; @@ -480,7 +481,7 @@ export class CellsAPI { ...(hasPublicLink !== undefined ? {HasPublicLink: hasPublicLink} : {}), }, Metadata: [ - ...createTagMetadataFilters(tags), + ...tagMetadataFilters, ...(mimeTypes?.map(term => ({Namespace: MIME_NAMESPACE, Term: term, Operation: mimeOp})) ?? []), ...(creatorIds?.map(term => ({ Namespace: USER_META_OWNER_UUID_NAMESPACE,