@memberjunction/ng-search 0.0.1 → 5.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/__tests__/search-types.test.d.ts +2 -0
  2. package/dist/__tests__/search-types.test.d.ts.map +1 -0
  3. package/dist/__tests__/search-types.test.js +107 -0
  4. package/dist/__tests__/search-types.test.js.map +1 -0
  5. package/dist/__tests__/search.service.test.d.ts +2 -0
  6. package/dist/__tests__/search.service.test.d.ts.map +1 -0
  7. package/dist/__tests__/search.service.test.js +183 -0
  8. package/dist/__tests__/search.service.test.js.map +1 -0
  9. package/dist/lib/module.d.ts +13 -0
  10. package/dist/lib/module.d.ts.map +1 -0
  11. package/dist/lib/module.js +43 -0
  12. package/dist/lib/module.js.map +1 -0
  13. package/dist/lib/search-filter.component.d.ts +39 -0
  14. package/dist/lib/search-filter.component.d.ts.map +1 -0
  15. package/dist/lib/search-filter.component.js +200 -0
  16. package/dist/lib/search-filter.component.js.map +1 -0
  17. package/dist/lib/search-overlay.component.d.ts +103 -0
  18. package/dist/lib/search-overlay.component.d.ts.map +1 -0
  19. package/dist/lib/search-overlay.component.js +626 -0
  20. package/dist/lib/search-overlay.component.js.map +1 -0
  21. package/dist/lib/search-results.component.d.ts +64 -0
  22. package/dist/lib/search-results.component.d.ts.map +1 -0
  23. package/dist/lib/search-results.component.js +378 -0
  24. package/dist/lib/search-results.component.js.map +1 -0
  25. package/dist/lib/search-types.d.ts +142 -0
  26. package/dist/lib/search-types.d.ts.map +1 -0
  27. package/dist/lib/search-types.js +8 -0
  28. package/dist/lib/search-types.js.map +1 -0
  29. package/dist/lib/search.service.d.ts +52 -0
  30. package/dist/lib/search.service.d.ts.map +1 -0
  31. package/dist/lib/search.service.js +287 -0
  32. package/dist/lib/search.service.js.map +1 -0
  33. package/dist/public-api.d.ts +7 -0
  34. package/dist/public-api.d.ts.map +1 -0
  35. package/dist/public-api.js +10 -0
  36. package/dist/public-api.js.map +1 -0
  37. package/package.json +38 -7
  38. package/README.md +0 -45
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search-types.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-types.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/search-types.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ describe('Search Types', () => {
3
+ describe('SearchResultItem', () => {
4
+ it('should be constructable with all required fields', () => {
5
+ const item = {
6
+ ID: 'test-1',
7
+ Title: 'Test Result',
8
+ Snippet: 'A snippet of matching text',
9
+ EntityName: 'Contacts',
10
+ RecordID: 'rec-123',
11
+ SourceType: 'entity',
12
+ Score: 0.85,
13
+ ScoreBreakdown: { Vector: 0.8, FullText: 0.9 },
14
+ Tags: ['customer', 'active'],
15
+ SourceIcon: 'fa-solid fa-database',
16
+ MatchedAt: new Date()
17
+ };
18
+ expect(item.ID).toBe('test-1');
19
+ expect(item.Score).toBe(0.85);
20
+ expect(item.Tags).toHaveLength(2);
21
+ expect(item.SourceType).toBe('entity');
22
+ });
23
+ });
24
+ describe('SearchResultGroup', () => {
25
+ it('should group results by source type', () => {
26
+ const group = {
27
+ Label: 'Entity Records',
28
+ Icon: 'fa-solid fa-database',
29
+ SourceType: 'entity',
30
+ Results: [],
31
+ TotalCount: 0
32
+ };
33
+ expect(group.SourceType).toBe('entity');
34
+ expect(group.Results).toHaveLength(0);
35
+ });
36
+ });
37
+ describe('SearchFilter', () => {
38
+ it('should support multi-select filter options', () => {
39
+ const filter = {
40
+ Category: 'Source Type',
41
+ Options: [
42
+ { Label: 'Entities', Value: 'entity', Count: 5, IsSelected: true },
43
+ { Label: 'Files', Value: 'file', Count: 3, IsSelected: false }
44
+ ],
45
+ MultiSelect: true
46
+ };
47
+ expect(filter.MultiSelect).toBe(true);
48
+ expect(filter.Options).toHaveLength(2);
49
+ expect(filter.Options[0].IsSelected).toBe(true);
50
+ });
51
+ });
52
+ describe('SearchRequest', () => {
53
+ it('should support filter configuration', () => {
54
+ const request = {
55
+ Query: 'test query',
56
+ MaxResults: 25,
57
+ ActiveFilters: {
58
+ 'Source Type': ['entity', 'file'],
59
+ 'Entity': ['Contacts']
60
+ },
61
+ IncludeSources: ['vector', 'fulltext']
62
+ };
63
+ expect(request.ActiveFilters['Source Type']).toHaveLength(2);
64
+ expect(request.IncludeSources).not.toContain('entity');
65
+ });
66
+ });
67
+ describe('SearchResponse', () => {
68
+ it('should include source counts', () => {
69
+ const response = {
70
+ Success: true,
71
+ Results: [],
72
+ Groups: [],
73
+ Filters: [],
74
+ TotalCount: 42,
75
+ ElapsedMs: 150,
76
+ SourceCounts: { Vector: 20, FullText: 15, Entity: 7 }
77
+ };
78
+ expect(response.Success).toBe(true);
79
+ expect(response.SourceCounts.Vector + response.SourceCounts.FullText + response.SourceCounts.Entity).toBe(42);
80
+ });
81
+ it('should support error state', () => {
82
+ const response = {
83
+ Success: false,
84
+ Results: [],
85
+ Groups: [],
86
+ Filters: [],
87
+ TotalCount: 0,
88
+ ElapsedMs: 0,
89
+ SourceCounts: { Vector: 0, FullText: 0, Entity: 0 },
90
+ ErrorMessage: 'Search service unavailable'
91
+ };
92
+ expect(response.Success).toBe(false);
93
+ expect(response.ErrorMessage).toBeDefined();
94
+ });
95
+ });
96
+ describe('ScoreBreakdown', () => {
97
+ it('should support partial score sources', () => {
98
+ const breakdown = {
99
+ Vector: 0.92
100
+ };
101
+ expect(breakdown.Vector).toBe(0.92);
102
+ expect(breakdown.FullText).toBeUndefined();
103
+ expect(breakdown.Entity).toBeUndefined();
104
+ });
105
+ });
106
+ });
107
+ //# sourceMappingURL=search-types.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-types.test.js","sourceRoot":"","sources":["../../src/__tests__/search-types.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAc9C,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC1B,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;YACxD,MAAM,IAAI,GAAqB;gBAC3B,EAAE,EAAE,QAAQ;gBACZ,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,4BAA4B;gBACrC,UAAU,EAAE,UAAU;gBACtB,QAAQ,EAAE,SAAS;gBACnB,UAAU,EAAE,QAAQ;gBACpB,KAAK,EAAE,IAAI;gBACX,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;gBAC9C,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;gBAC5B,UAAU,EAAE,sBAAsB;gBAClC,SAAS,EAAE,IAAI,IAAI,EAAE;aACxB,CAAC;YAEF,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC3C,MAAM,KAAK,GAAsB;gBAC7B,KAAK,EAAE,gBAAgB;gBACvB,IAAI,EAAE,sBAAsB;gBAC5B,UAAU,EAAE,QAAQ;gBACpB,OAAO,EAAE,EAAE;gBACX,UAAU,EAAE,CAAC;aAChB,CAAC;YAEF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;YAClD,MAAM,MAAM,GAAiB;gBACzB,QAAQ,EAAE,aAAa;gBACvB,OAAO,EAAE;oBACL,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE;oBAClE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE;iBACjE;gBACD,WAAW,EAAE,IAAI;aACpB,CAAC;YAEF,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC3B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC3C,MAAM,OAAO,GAAkB;gBAC3B,KAAK,EAAE,YAAY;gBACnB,UAAU,EAAE,EAAE;gBACd,aAAa,EAAE;oBACX,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;oBACjC,QAAQ,EAAE,CAAC,UAAU,CAAC;iBACzB;gBACD,cAAc,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC;aACzC,CAAC;YAEF,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;YACpC,MAAM,QAAQ,GAAmB;gBAC7B,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,EAAE;gBACX,MAAM,EAAE,EAAE;gBACV,OAAO,EAAE,EAAE;gBACX,UAAU,EAAE,EAAE;gBACd,SAAS,EAAE,GAAG;gBACd,YAAY,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;aACxD,CAAC;YAEF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YAClC,MAAM,QAAQ,GAAmB;gBAC7B,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,EAAE;gBACX,MAAM,EAAE,EAAE;gBACV,OAAO,EAAE,EAAE;gBACX,UAAU,EAAE,CAAC;gBACb,SAAS,EAAE,CAAC;gBACZ,YAAY,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;gBACnD,YAAY,EAAE,4BAA4B;aAC7C,CAAC;YAEF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC;QAChD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;YAC5C,MAAM,SAAS,GAAmB;gBAC9B,MAAM,EAAE,IAAI;aACf,CAAC;YAEF,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;YAC3C,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,CAAC;QAC7C,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport {\n SearchResultItem,\n SearchResultGroup,\n SearchFilter,\n SearchFilterOption,\n SearchRequest,\n SearchResponse,\n SearchResultSelectedEvent,\n SearchFilterChangeEvent,\n SearchExecutedEvent,\n ScoreBreakdown\n} from '../lib/search-types';\n\ndescribe('Search Types', () => {\n describe('SearchResultItem', () => {\n it('should be constructable with all required fields', () => {\n const item: SearchResultItem = {\n ID: 'test-1',\n Title: 'Test Result',\n Snippet: 'A snippet of matching text',\n EntityName: 'Contacts',\n RecordID: 'rec-123',\n SourceType: 'entity',\n Score: 0.85,\n ScoreBreakdown: { Vector: 0.8, FullText: 0.9 },\n Tags: ['customer', 'active'],\n SourceIcon: 'fa-solid fa-database',\n MatchedAt: new Date()\n };\n\n expect(item.ID).toBe('test-1');\n expect(item.Score).toBe(0.85);\n expect(item.Tags).toHaveLength(2);\n expect(item.SourceType).toBe('entity');\n });\n });\n\n describe('SearchResultGroup', () => {\n it('should group results by source type', () => {\n const group: SearchResultGroup = {\n Label: 'Entity Records',\n Icon: 'fa-solid fa-database',\n SourceType: 'entity',\n Results: [],\n TotalCount: 0\n };\n\n expect(group.SourceType).toBe('entity');\n expect(group.Results).toHaveLength(0);\n });\n });\n\n describe('SearchFilter', () => {\n it('should support multi-select filter options', () => {\n const filter: SearchFilter = {\n Category: 'Source Type',\n Options: [\n { Label: 'Entities', Value: 'entity', Count: 5, IsSelected: true },\n { Label: 'Files', Value: 'file', Count: 3, IsSelected: false }\n ],\n MultiSelect: true\n };\n\n expect(filter.MultiSelect).toBe(true);\n expect(filter.Options).toHaveLength(2);\n expect(filter.Options[0].IsSelected).toBe(true);\n });\n });\n\n describe('SearchRequest', () => {\n it('should support filter configuration', () => {\n const request: SearchRequest = {\n Query: 'test query',\n MaxResults: 25,\n ActiveFilters: {\n 'Source Type': ['entity', 'file'],\n 'Entity': ['Contacts']\n },\n IncludeSources: ['vector', 'fulltext']\n };\n\n expect(request.ActiveFilters['Source Type']).toHaveLength(2);\n expect(request.IncludeSources).not.toContain('entity');\n });\n });\n\n describe('SearchResponse', () => {\n it('should include source counts', () => {\n const response: SearchResponse = {\n Success: true,\n Results: [],\n Groups: [],\n Filters: [],\n TotalCount: 42,\n ElapsedMs: 150,\n SourceCounts: { Vector: 20, FullText: 15, Entity: 7 }\n };\n\n expect(response.Success).toBe(true);\n expect(response.SourceCounts.Vector + response.SourceCounts.FullText + response.SourceCounts.Entity).toBe(42);\n });\n\n it('should support error state', () => {\n const response: SearchResponse = {\n Success: false,\n Results: [],\n Groups: [],\n Filters: [],\n TotalCount: 0,\n ElapsedMs: 0,\n SourceCounts: { Vector: 0, FullText: 0, Entity: 0 },\n ErrorMessage: 'Search service unavailable'\n };\n\n expect(response.Success).toBe(false);\n expect(response.ErrorMessage).toBeDefined();\n });\n });\n\n describe('ScoreBreakdown', () => {\n it('should support partial score sources', () => {\n const breakdown: ScoreBreakdown = {\n Vector: 0.92\n };\n\n expect(breakdown.Vector).toBe(0.92);\n expect(breakdown.FullText).toBeUndefined();\n expect(breakdown.Entity).toBeUndefined();\n });\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search.service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/search.service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { Metadata } from '@memberjunction/core';
3
+ import { SearchService } from '../lib/search.service';
4
+ /** Installs a fake Metadata.Provider with a mock ExecuteGQL that returns empty success */
5
+ function installMockProvider() {
6
+ const mockExecuteGQL = vi.fn().mockResolvedValue({
7
+ SearchKnowledge: {
8
+ Success: true,
9
+ Results: [],
10
+ TotalCount: 0,
11
+ ElapsedMs: 1,
12
+ SourceCounts: { Vector: 0, FullText: 0, Entity: 0 },
13
+ }
14
+ });
15
+ Metadata['Provider'] = { ExecuteGQL: mockExecuteGQL };
16
+ return mockExecuteGQL;
17
+ }
18
+ describe('SearchService', () => {
19
+ let service;
20
+ beforeEach(() => {
21
+ service = new SearchService();
22
+ installMockProvider();
23
+ });
24
+ describe('ExecuteSearch', () => {
25
+ it('should set IsSearching to true during search', async () => {
26
+ const values = [];
27
+ service.IsSearching$.subscribe(val => values.push(val));
28
+ await service.ExecuteSearch({
29
+ Query: 'test query',
30
+ MaxResults: 10,
31
+ ActiveFilters: {},
32
+ IncludeSources: ['vector', 'fulltext', 'entity']
33
+ });
34
+ // Should have gone true then false
35
+ expect(values).toContain(true);
36
+ expect(values[values.length - 1]).toBe(false);
37
+ });
38
+ it('should return empty results for empty query', async () => {
39
+ const response = await service.ExecuteSearch({
40
+ Query: ' ',
41
+ MaxResults: 10,
42
+ ActiveFilters: {},
43
+ IncludeSources: ['vector']
44
+ });
45
+ expect(response.Success).toBe(true);
46
+ expect(response.Results).toHaveLength(0);
47
+ expect(response.TotalCount).toBe(0);
48
+ });
49
+ it('should update SearchResults$ observable', async () => {
50
+ await service.ExecuteSearch({
51
+ Query: 'test',
52
+ MaxResults: 10,
53
+ ActiveFilters: {},
54
+ IncludeSources: ['vector']
55
+ });
56
+ const result = service.SearchResults$.value;
57
+ expect(result).not.toBeNull();
58
+ expect(result?.Success).toBe(true);
59
+ });
60
+ it('should add to recent searches', async () => {
61
+ await service.ExecuteSearch({
62
+ Query: 'test query',
63
+ MaxResults: 10,
64
+ ActiveFilters: {},
65
+ IncludeSources: ['vector']
66
+ });
67
+ const recent = service.RecentSearches$.value;
68
+ expect(recent.length).toBe(1);
69
+ expect(recent[0].Query).toBe('test query');
70
+ });
71
+ it('should deduplicate recent searches', async () => {
72
+ const request = {
73
+ Query: 'test',
74
+ MaxResults: 10,
75
+ ActiveFilters: {},
76
+ IncludeSources: ['vector']
77
+ };
78
+ await service.ExecuteSearch(request);
79
+ await service.ExecuteSearch(request);
80
+ const recent = service.RecentSearches$.value;
81
+ expect(recent.length).toBe(1);
82
+ });
83
+ it('should limit recent searches to max 20', async () => {
84
+ for (let i = 0; i < 25; i++) {
85
+ await service.ExecuteSearch({
86
+ Query: `query ${i}`,
87
+ MaxResults: 10,
88
+ ActiveFilters: {},
89
+ IncludeSources: ['vector']
90
+ });
91
+ }
92
+ expect(service.RecentSearches$.value.length).toBeLessThanOrEqual(20);
93
+ });
94
+ });
95
+ describe('ClearResults', () => {
96
+ it('should set SearchResults$ to null', async () => {
97
+ await service.ExecuteSearch({
98
+ Query: 'test',
99
+ MaxResults: 10,
100
+ ActiveFilters: {},
101
+ IncludeSources: ['vector']
102
+ });
103
+ service.ClearResults();
104
+ expect(service.SearchResults$.value).toBeNull();
105
+ });
106
+ });
107
+ describe('ClearRecentSearches', () => {
108
+ it('should clear all recent searches', async () => {
109
+ await service.ExecuteSearch({
110
+ Query: 'test',
111
+ MaxResults: 10,
112
+ ActiveFilters: {},
113
+ IncludeSources: ['vector']
114
+ });
115
+ service.ClearRecentSearches();
116
+ expect(service.RecentSearches$.value).toHaveLength(0);
117
+ });
118
+ });
119
+ describe('GroupResults', () => {
120
+ it('should group results by source type', () => {
121
+ const results = [
122
+ createMockResult('1', 'entity', 'Contacts'),
123
+ createMockResult('2', 'entity', 'Contacts'),
124
+ createMockResult('3', 'file', 'Documents'),
125
+ createMockResult('4', 'web-page', 'Pages'),
126
+ ];
127
+ const groups = service.GroupResults(results);
128
+ expect(groups).toHaveLength(3);
129
+ const entityGroup = groups.find(g => g.SourceType === 'entity');
130
+ expect(entityGroup?.Results).toHaveLength(2);
131
+ expect(entityGroup?.Label).toBe('Entity Records');
132
+ });
133
+ it('should return empty array for empty input', () => {
134
+ const groups = service.GroupResults([]);
135
+ expect(groups).toHaveLength(0);
136
+ });
137
+ });
138
+ describe('BuildFilters', () => {
139
+ it('should build source type, entity, and tag filters', () => {
140
+ const results = [
141
+ createMockResult('1', 'entity', 'Contacts', ['customer']),
142
+ createMockResult('2', 'file', 'Documents', ['internal']),
143
+ ];
144
+ const filters = service.BuildFilters(results);
145
+ expect(filters).toHaveLength(3);
146
+ const sourceFilter = filters.find(f => f.Category === 'Source Type');
147
+ expect(sourceFilter?.Options).toHaveLength(2);
148
+ });
149
+ it('should limit tags filter to 15 options', () => {
150
+ const tags = Array.from({ length: 20 }, (_, i) => `tag${i}`);
151
+ const results = tags.map((tag, i) => createMockResult(String(i), 'entity', 'Test', [tag]));
152
+ const filters = service.BuildFilters(results);
153
+ const tagFilter = filters.find(f => f.Category === 'Tags');
154
+ expect(tagFilter.Options.length).toBeLessThanOrEqual(15);
155
+ });
156
+ });
157
+ describe('GetSourceIcon', () => {
158
+ it('should return correct icon for known types', () => {
159
+ expect(service.GetSourceIcon('entity')).toContain('fa-database');
160
+ expect(service.GetSourceIcon('file')).toContain('fa-file');
161
+ expect(service.GetSourceIcon('web-page')).toContain('fa-globe');
162
+ });
163
+ it('should return fallback icon for unknown types', () => {
164
+ expect(service.GetSourceIcon('unknown')).toContain('fa-circle');
165
+ });
166
+ });
167
+ });
168
+ function createMockResult(id, sourceType, entityName, tags = []) {
169
+ return {
170
+ ID: id,
171
+ Title: `Result ${id}`,
172
+ Snippet: `Snippet for result ${id}`,
173
+ EntityName: entityName,
174
+ RecordID: `rec-${id}`,
175
+ SourceType: sourceType,
176
+ Score: 0.85,
177
+ ScoreBreakdown: { Vector: 0.8, FullText: 0.9 },
178
+ Tags: tags,
179
+ SourceIcon: 'fa-solid fa-database',
180
+ MatchedAt: new Date()
181
+ };
182
+ }
183
+ //# sourceMappingURL=search.service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.service.test.js","sourceRoot":"","sources":["../../src/__tests__/search.service.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { Metadata } from '@memberjunction/core';\nimport { SearchService, RecentSearch } from '../lib/search.service';\nimport {\n SearchRequest,\n SearchResponse,\n SearchResultItem,\n SearchResultGroup,\n SearchFilter\n} from '../lib/search-types';\n\n/** Installs a fake Metadata.Provider with a mock ExecuteGQL that returns empty success */\nfunction installMockProvider(): ReturnType<typeof vi.fn> {\n const mockExecuteGQL = vi.fn().mockResolvedValue({\n SearchKnowledge: {\n Success: true,\n Results: [],\n TotalCount: 0,\n ElapsedMs: 1,\n SourceCounts: { Vector: 0, FullText: 0, Entity: 0 },\n }\n });\n (Metadata as unknown as Record<string, unknown>)['Provider'] = { ExecuteGQL: mockExecuteGQL };\n return mockExecuteGQL;\n}\n\ndescribe('SearchService', () => {\n let service: SearchService;\n\n beforeEach(() => {\n service = new SearchService();\n installMockProvider();\n });\n\n describe('ExecuteSearch', () => {\n it('should set IsSearching to true during search', async () => {\n const values: boolean[] = [];\n service.IsSearching$.subscribe(val => values.push(val));\n\n await service.ExecuteSearch({\n Query: 'test query',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector', 'fulltext', 'entity']\n });\n\n // Should have gone true then false\n expect(values).toContain(true);\n expect(values[values.length - 1]).toBe(false);\n });\n\n it('should return empty results for empty query', async () => {\n const response = await service.ExecuteSearch({\n Query: ' ',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n\n expect(response.Success).toBe(true);\n expect(response.Results).toHaveLength(0);\n expect(response.TotalCount).toBe(0);\n });\n\n it('should update SearchResults$ observable', async () => {\n await service.ExecuteSearch({\n Query: 'test',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n\n const result = service.SearchResults$.value;\n expect(result).not.toBeNull();\n expect(result?.Success).toBe(true);\n });\n\n it('should add to recent searches', async () => {\n await service.ExecuteSearch({\n Query: 'test query',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n\n const recent = service.RecentSearches$.value;\n expect(recent.length).toBe(1);\n expect(recent[0].Query).toBe('test query');\n });\n\n it('should deduplicate recent searches', async () => {\n const request: SearchRequest = {\n Query: 'test',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n };\n\n await service.ExecuteSearch(request);\n await service.ExecuteSearch(request);\n\n const recent = service.RecentSearches$.value;\n expect(recent.length).toBe(1);\n });\n\n it('should limit recent searches to max 20', async () => {\n for (let i = 0; i < 25; i++) {\n await service.ExecuteSearch({\n Query: `query ${i}`,\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n }\n\n expect(service.RecentSearches$.value.length).toBeLessThanOrEqual(20);\n });\n });\n\n describe('ClearResults', () => {\n it('should set SearchResults$ to null', async () => {\n await service.ExecuteSearch({\n Query: 'test',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n\n service.ClearResults();\n expect(service.SearchResults$.value).toBeNull();\n });\n });\n\n describe('ClearRecentSearches', () => {\n it('should clear all recent searches', async () => {\n await service.ExecuteSearch({\n Query: 'test',\n MaxResults: 10,\n ActiveFilters: {},\n IncludeSources: ['vector']\n });\n\n service.ClearRecentSearches();\n expect(service.RecentSearches$.value).toHaveLength(0);\n });\n });\n\n describe('GroupResults', () => {\n it('should group results by source type', () => {\n const results: SearchResultItem[] = [\n createMockResult('1', 'entity', 'Contacts'),\n createMockResult('2', 'entity', 'Contacts'),\n createMockResult('3', 'file', 'Documents'),\n createMockResult('4', 'web-page', 'Pages'),\n ];\n\n const groups = service.GroupResults(results);\n\n expect(groups).toHaveLength(3);\n const entityGroup = groups.find(g => g.SourceType === 'entity');\n expect(entityGroup?.Results).toHaveLength(2);\n expect(entityGroup?.Label).toBe('Entity Records');\n });\n\n it('should return empty array for empty input', () => {\n const groups = service.GroupResults([]);\n expect(groups).toHaveLength(0);\n });\n });\n\n describe('BuildFilters', () => {\n it('should build source type, entity, and tag filters', () => {\n const results: SearchResultItem[] = [\n createMockResult('1', 'entity', 'Contacts', ['customer']),\n createMockResult('2', 'file', 'Documents', ['internal']),\n ];\n\n const filters = service.BuildFilters(results);\n\n expect(filters).toHaveLength(3);\n const sourceFilter = filters.find(f => f.Category === 'Source Type');\n expect(sourceFilter?.Options).toHaveLength(2);\n });\n\n it('should limit tags filter to 15 options', () => {\n const tags = Array.from({ length: 20 }, (_, i) => `tag${i}`);\n const results = tags.map((tag, i) =>\n createMockResult(String(i), 'entity', 'Test', [tag])\n );\n\n const filters = service.BuildFilters(results);\n const tagFilter = filters.find(f => f.Category === 'Tags');\n expect(tagFilter!.Options.length).toBeLessThanOrEqual(15);\n });\n });\n\n describe('GetSourceIcon', () => {\n it('should return correct icon for known types', () => {\n expect(service.GetSourceIcon('entity')).toContain('fa-database');\n expect(service.GetSourceIcon('file')).toContain('fa-file');\n expect(service.GetSourceIcon('web-page')).toContain('fa-globe');\n });\n\n it('should return fallback icon for unknown types', () => {\n expect(service.GetSourceIcon('unknown')).toContain('fa-circle');\n });\n });\n});\n\nfunction createMockResult(\n id: string,\n sourceType: 'entity' | 'content-item' | 'file' | 'web-page',\n entityName: string,\n tags: string[] = []\n): SearchResultItem {\n return {\n ID: id,\n Title: `Result ${id}`,\n Snippet: `Snippet for result ${id}`,\n EntityName: entityName,\n RecordID: `rec-${id}`,\n SourceType: sourceType,\n Score: 0.85,\n ScoreBreakdown: { Vector: 0.8, FullText: 0.9 },\n Tags: tags,\n SourceIcon: 'fa-solid fa-database',\n MatchedAt: new Date()\n };\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import * as i0 from "@angular/core";
2
+ import * as i1 from "./search-overlay.component";
3
+ import * as i2 from "./search-results.component";
4
+ import * as i3 from "./search-filter.component";
5
+ import * as i4 from "@angular/common";
6
+ import * as i5 from "@angular/forms";
7
+ import * as i6 from "@memberjunction/ng-shared-generic";
8
+ export declare class SearchModule {
9
+ static ɵfac: i0.ɵɵFactoryDeclaration<SearchModule, never>;
10
+ static ɵmod: i0.ɵɵNgModuleDeclaration<SearchModule, [typeof i1.SearchOverlayComponent, typeof i2.SearchResultsComponent, typeof i3.SearchFilterComponent], [typeof i4.CommonModule, typeof i5.FormsModule, typeof i6.SharedGenericModule], [typeof i1.SearchOverlayComponent, typeof i2.SearchResultsComponent, typeof i3.SearchFilterComponent]>;
11
+ static ɵinj: i0.ɵɵInjectorDeclaration<SearchModule>;
12
+ }
13
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/lib/module.ts"],"names":[],"mappings":";;;;;;;AASA,qBAiBa,YAAY;yCAAZ,YAAY;0CAAZ,YAAY;0CAAZ,YAAY;CAAI"}
@@ -0,0 +1,43 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { SharedGenericModule } from '@memberjunction/ng-shared-generic';
5
+ import { SearchOverlayComponent } from './search-overlay.component';
6
+ import { SearchResultsComponent } from './search-results.component';
7
+ import { SearchFilterComponent } from './search-filter.component';
8
+ import * as i0 from "@angular/core";
9
+ export class SearchModule {
10
+ static ɵfac = function SearchModule_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SearchModule)(); };
11
+ static ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: SearchModule });
12
+ static ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [CommonModule,
13
+ FormsModule,
14
+ SharedGenericModule] });
15
+ }
16
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SearchModule, [{
17
+ type: NgModule,
18
+ args: [{
19
+ declarations: [
20
+ SearchOverlayComponent,
21
+ SearchResultsComponent,
22
+ SearchFilterComponent
23
+ ],
24
+ imports: [
25
+ CommonModule,
26
+ FormsModule,
27
+ SharedGenericModule
28
+ ],
29
+ exports: [
30
+ SearchOverlayComponent,
31
+ SearchResultsComponent,
32
+ SearchFilterComponent
33
+ ]
34
+ }]
35
+ }], null, null); })();
36
+ (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(SearchModule, { declarations: [SearchOverlayComponent,
37
+ SearchResultsComponent,
38
+ SearchFilterComponent], imports: [CommonModule,
39
+ FormsModule,
40
+ SharedGenericModule], exports: [SearchOverlayComponent,
41
+ SearchResultsComponent,
42
+ SearchFilterComponent] }); })();
43
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../src/lib/module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAExE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;;AAmBlE,MAAM,OAAO,YAAY;sGAAZ,YAAY;4DAAZ,YAAY;gEAVjB,YAAY;YACZ,WAAW;YACX,mBAAmB;;iFAQd,YAAY;cAjBxB,QAAQ;eAAC;gBACN,YAAY,EAAE;oBACV,sBAAsB;oBACtB,sBAAsB;oBACtB,qBAAqB;iBACxB;gBACD,OAAO,EAAE;oBACL,YAAY;oBACZ,WAAW;oBACX,mBAAmB;iBACtB;gBACD,OAAO,EAAE;oBACL,sBAAsB;oBACtB,sBAAsB;oBACtB,qBAAqB;iBACxB;aACJ;;wFACY,YAAY,mBAfjB,sBAAsB;QACtB,sBAAsB;QACtB,qBAAqB,aAGrB,YAAY;QACZ,WAAW;QACX,mBAAmB,aAGnB,sBAAsB;QACtB,sBAAsB;QACtB,qBAAqB","sourcesContent":["import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { SharedGenericModule } from '@memberjunction/ng-shared-generic';\n\nimport { SearchOverlayComponent } from './search-overlay.component';\nimport { SearchResultsComponent } from './search-results.component';\nimport { SearchFilterComponent } from './search-filter.component';\n\n@NgModule({\n declarations: [\n SearchOverlayComponent,\n SearchResultsComponent,\n SearchFilterComponent\n ],\n imports: [\n CommonModule,\n FormsModule,\n SharedGenericModule\n ],\n exports: [\n SearchOverlayComponent,\n SearchResultsComponent,\n SearchFilterComponent\n ]\n})\nexport class SearchModule { }\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Search Filter Component
3
+ *
4
+ * Faceted filter sidebar for narrowing search results.
5
+ * Displays filter categories with checkable options and counts.
6
+ */
7
+ import { EventEmitter } from '@angular/core';
8
+ import { SearchFilter, SearchFilterChangeEvent } from './search-types';
9
+ import * as i0 from "@angular/core";
10
+ export declare class SearchFilterComponent {
11
+ private cdr;
12
+ /** Available filters to display */
13
+ Filters: SearchFilter[];
14
+ /** Currently active filter selections */
15
+ ActiveFilters: Record<string, string[]>;
16
+ /** Whether the filter panel is collapsible */
17
+ Collapsible: boolean;
18
+ FilterChanged: EventEmitter<SearchFilterChangeEvent>;
19
+ FiltersCleared: EventEmitter<void>;
20
+ /** Collapsed state per category */
21
+ CollapsedCategories: Set<string>;
22
+ /** Toggle a filter option */
23
+ ToggleOption(category: string, value: string): void;
24
+ /** Toggle category collapse */
25
+ ToggleCategory(category: string): void;
26
+ /** Check if a category is collapsed */
27
+ IsCategoryCollapsed(category: string): boolean;
28
+ /** Check if an option is selected */
29
+ IsOptionSelected(category: string, value: string): boolean;
30
+ /** Check if any filters are active */
31
+ HasActiveFilters(): boolean;
32
+ /** Get total active filter count */
33
+ GetActiveFilterCount(): number;
34
+ /** Clear all filters */
35
+ ClearAll(): void;
36
+ static ɵfac: i0.ɵɵFactoryDeclaration<SearchFilterComponent, never>;
37
+ static ɵcmp: i0.ɵɵComponentDeclaration<SearchFilterComponent, "mj-search-filter", never, { "Filters": { "alias": "Filters"; "required": false; }; "ActiveFilters": { "alias": "ActiveFilters"; "required": false; }; "Collapsible": { "alias": "Collapsible"; "required": false; }; }, { "FilterChanged": "FilterChanged"; "FiltersCleared": "FiltersCleared"; }, never, never, false, never>;
38
+ }
39
+ //# sourceMappingURL=search-filter.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-filter.component.d.ts","sourceRoot":"","sources":["../../src/lib/search-filter.component.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEH,YAAY,EAKf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;;AAEvE,qBAMa,qBAAqB;IAC9B,OAAO,CAAC,GAAG,CAA6B;IAExC,mCAAmC;IAC1B,OAAO,EAAE,YAAY,EAAE,CAAM;IAEtC,yCAAyC;IAChC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAM;IAEtD,8CAA8C;IACrC,WAAW,UAAQ;IAElB,aAAa,wCAA+C;IAC5D,cAAc,qBAA4B;IAEpD,mCAAmC;IAC5B,mBAAmB,cAAqB;IAE/C,6BAA6B;IACtB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAc1D,+BAA+B;IACxB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU7C,uCAAuC;IAChC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIrD,qCAAqC;IAC9B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAIjE,sCAAsC;IAC/B,gBAAgB,IAAI,OAAO;IAIlC,oCAAoC;IAC7B,oBAAoB,IAAI,MAAM;IAIrC,wBAAwB;IACjB,QAAQ,IAAI,IAAI;yCAjEd,qBAAqB;2CAArB,qBAAqB;CAsEjC"}