@finos/legend-application-marketplace 0.2.2 → 0.2.4

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 (137) hide show
  1. package/lib/__lib__/LegendMarketplaceAppEvent.d.ts +3 -0
  2. package/lib/__lib__/LegendMarketplaceAppEvent.d.ts.map +1 -1
  3. package/lib/__lib__/LegendMarketplaceAppEvent.js +3 -0
  4. package/lib/__lib__/LegendMarketplaceAppEvent.js.map +1 -1
  5. package/lib/__lib__/LegendMarketplaceNavigation.d.ts +7 -1
  6. package/lib/__lib__/LegendMarketplaceNavigation.d.ts.map +1 -1
  7. package/lib/__lib__/LegendMarketplaceNavigation.js +9 -1
  8. package/lib/__lib__/LegendMarketplaceNavigation.js.map +1 -1
  9. package/lib/__lib__/LegendMarketplaceTelemetryHelper.d.ts +3 -1
  10. package/lib/__lib__/LegendMarketplaceTelemetryHelper.d.ts.map +1 -1
  11. package/lib/__lib__/LegendMarketplaceTelemetryHelper.js +18 -2
  12. package/lib/__lib__/LegendMarketplaceTelemetryHelper.js.map +1 -1
  13. package/lib/application/LegendMarketplaceWebApplication.d.ts.map +1 -1
  14. package/lib/application/LegendMarketplaceWebApplication.js +4 -1
  15. package/lib/application/LegendMarketplaceWebApplication.js.map +1 -1
  16. package/lib/application/providers/LegendMarketplaceFieldSearchResultsStoreProvider.d.ts +22 -0
  17. package/lib/application/providers/LegendMarketplaceFieldSearchResultsStoreProvider.d.ts.map +1 -0
  18. package/lib/application/providers/LegendMarketplaceFieldSearchResultsStoreProvider.js +37 -0
  19. package/lib/application/providers/LegendMarketplaceFieldSearchResultsStoreProvider.js.map +1 -0
  20. package/lib/components/AddToCart/CartDrawer.d.ts.map +1 -1
  21. package/lib/components/AddToCart/CartDrawer.js +36 -4
  22. package/lib/components/AddToCart/CartDrawer.js.map +1 -1
  23. package/lib/components/AddToCart/RecommendedAddOnsModal.d.ts +2 -1
  24. package/lib/components/AddToCart/RecommendedAddOnsModal.d.ts.map +1 -1
  25. package/lib/components/AddToCart/RecommendedAddOnsModal.js +23 -13
  26. package/lib/components/AddToCart/RecommendedAddOnsModal.js.map +1 -1
  27. package/lib/components/AddToCart/RecommendedItemsCard.d.ts +3 -1
  28. package/lib/components/AddToCart/RecommendedItemsCard.d.ts.map +1 -1
  29. package/lib/components/AddToCart/RecommendedItemsCard.js +14 -11
  30. package/lib/components/AddToCart/RecommendedItemsCard.js.map +1 -1
  31. package/lib/components/FieldSearchFiltersPanel/FieldSearchFiltersPanel.d.ts +23 -0
  32. package/lib/components/FieldSearchFiltersPanel/FieldSearchFiltersPanel.d.ts.map +1 -0
  33. package/lib/components/FieldSearchFiltersPanel/FieldSearchFiltersPanel.js +22 -0
  34. package/lib/components/FieldSearchFiltersPanel/FieldSearchFiltersPanel.js.map +1 -0
  35. package/lib/components/LegendServiceCard/LegendServiceCard.d.ts +2 -0
  36. package/lib/components/LegendServiceCard/LegendServiceCard.d.ts.map +1 -1
  37. package/lib/components/LegendServiceCard/LegendServiceCard.js +12 -8
  38. package/lib/components/LegendServiceCard/LegendServiceCard.js.map +1 -1
  39. package/lib/components/LegendServiceCard/LegendServiceGrid.d.ts +24 -0
  40. package/lib/components/LegendServiceCard/LegendServiceGrid.d.ts.map +1 -0
  41. package/lib/components/LegendServiceCard/LegendServiceGrid.js +124 -0
  42. package/lib/components/LegendServiceCard/LegendServiceGrid.js.map +1 -0
  43. package/lib/components/LegendServiceCard/LegendServiceListRow.d.ts +2 -0
  44. package/lib/components/LegendServiceCard/LegendServiceListRow.d.ts.map +1 -1
  45. package/lib/components/LegendServiceCard/LegendServiceListRow.js +8 -4
  46. package/lib/components/LegendServiceCard/LegendServiceListRow.js.map +1 -1
  47. package/lib/components/MarketplaceCard/FieldSearchResultListItem.d.ts +25 -0
  48. package/lib/components/MarketplaceCard/FieldSearchResultListItem.d.ts.map +1 -0
  49. package/lib/components/MarketplaceCard/FieldSearchResultListItem.js +58 -0
  50. package/lib/components/MarketplaceCard/FieldSearchResultListItem.js.map +1 -0
  51. package/lib/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.d.ts +10 -0
  52. package/lib/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.d.ts.map +1 -1
  53. package/lib/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.js +2 -2
  54. package/lib/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.js.map +1 -1
  55. package/lib/components/Pagination/PaginationControls.d.ts +1 -0
  56. package/lib/components/Pagination/PaginationControls.d.ts.map +1 -1
  57. package/lib/components/Pagination/PaginationControls.js +2 -2
  58. package/lib/components/Pagination/PaginationControls.js.map +1 -1
  59. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.d.ts.map +1 -1
  60. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.js +5 -2
  61. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.js.map +1 -1
  62. package/lib/components/SearchBar/LegendMarketplaceSearchBar.d.ts +2 -1
  63. package/lib/components/SearchBar/LegendMarketplaceSearchBar.d.ts.map +1 -1
  64. package/lib/components/SearchBar/LegendMarketplaceSearchBar.js +20 -7
  65. package/lib/components/SearchBar/LegendMarketplaceSearchBar.js.map +1 -1
  66. package/lib/index.css +2 -2
  67. package/lib/index.css.map +1 -1
  68. package/lib/package.json +1 -1
  69. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.d.ts.map +1 -1
  70. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.js +17 -11
  71. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.js.map +1 -1
  72. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.d.ts.map +1 -1
  73. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.js +10 -5
  74. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.js.map +1 -1
  75. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.d.ts +17 -0
  76. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.d.ts.map +1 -0
  77. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.js +126 -0
  78. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.js.map +1 -0
  79. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.d.ts.map +1 -1
  80. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js +14 -5
  81. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js.map +1 -1
  82. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.d.ts.map +1 -1
  83. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.js +2 -2
  84. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.js.map +1 -1
  85. package/lib/stores/cart/CartStore.d.ts +10 -3
  86. package/lib/stores/cart/CartStore.d.ts.map +1 -1
  87. package/lib/stores/cart/CartStore.js +66 -42
  88. package/lib/stores/cart/CartStore.js.map +1 -1
  89. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.d.ts +8 -1
  90. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.d.ts.map +1 -1
  91. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.js +37 -3
  92. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.js.map +1 -1
  93. package/lib/stores/lakehouse/LegendMarketplaceFieldSearchResultsStore.d.ts +63 -0
  94. package/lib/stores/lakehouse/LegendMarketplaceFieldSearchResultsStore.d.ts.map +1 -0
  95. package/lib/stores/lakehouse/LegendMarketplaceFieldSearchResultsStore.js +228 -0
  96. package/lib/stores/lakehouse/LegendMarketplaceFieldSearchResultsStore.js.map +1 -0
  97. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.d.ts.map +1 -1
  98. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.js +9 -13
  99. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.js.map +1 -1
  100. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.d.ts +2 -0
  101. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.d.ts.map +1 -1
  102. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.js +8 -4
  103. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.js.map +1 -1
  104. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.d.ts +40 -0
  105. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.d.ts.map +1 -0
  106. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.js +84 -0
  107. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.js.map +1 -0
  108. package/package.json +13 -13
  109. package/src/__lib__/LegendMarketplaceAppEvent.ts +3 -0
  110. package/src/__lib__/LegendMarketplaceNavigation.ts +18 -1
  111. package/src/__lib__/LegendMarketplaceTelemetryHelper.ts +32 -1
  112. package/src/application/LegendMarketplaceWebApplication.tsx +13 -0
  113. package/src/application/providers/LegendMarketplaceFieldSearchResultsStoreProvider.tsx +67 -0
  114. package/src/components/AddToCart/CartDrawer.tsx +49 -4
  115. package/src/components/AddToCart/RecommendedAddOnsModal.tsx +86 -24
  116. package/src/components/AddToCart/RecommendedItemsCard.tsx +143 -120
  117. package/src/components/FieldSearchFiltersPanel/FieldSearchFiltersPanel.tsx +65 -0
  118. package/src/components/LegendServiceCard/LegendServiceCard.tsx +25 -3
  119. package/src/components/LegendServiceCard/LegendServiceGrid.tsx +206 -0
  120. package/src/components/LegendServiceCard/LegendServiceListRow.tsx +27 -3
  121. package/src/components/MarketplaceCard/FieldSearchResultListItem.tsx +163 -0
  122. package/src/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.tsx +2 -2
  123. package/src/components/Pagination/PaginationControls.tsx +7 -4
  124. package/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx +7 -0
  125. package/src/components/SearchBar/LegendMarketplaceSearchBar.tsx +44 -3
  126. package/src/pages/DataAPIs/LegendMarketplaceDataAPIs.tsx +80 -14
  127. package/src/pages/Lakehouse/MarketplaceLakehouseHome.tsx +16 -3
  128. package/src/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.tsx +380 -0
  129. package/src/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.tsx +34 -3
  130. package/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx +6 -2
  131. package/src/stores/cart/CartStore.ts +86 -51
  132. package/src/stores/dataAPIs/LegendMarketplaceDataAPIsStore.ts +58 -2
  133. package/src/stores/lakehouse/LegendMarketplaceFieldSearchResultsStore.ts +309 -0
  134. package/src/stores/lakehouse/LegendMarketplaceProductViewerStore.ts +23 -30
  135. package/src/stores/lakehouse/LegendMarketplaceSearchResultsStore.ts +11 -6
  136. package/src/stores/lakehouse/fieldSearch/FieldSearchResultState.ts +122 -0
  137. package/tsconfig.json +7 -0
@@ -14,7 +14,14 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { makeObservable, observable, action, flow, flowResult } from 'mobx';
17
+ import {
18
+ makeObservable,
19
+ observable,
20
+ action,
21
+ flow,
22
+ flowResult,
23
+ computed,
24
+ } from 'mobx';
18
25
  import {
19
26
  LogEvent,
20
27
  type GeneratorFn,
@@ -22,6 +29,7 @@ import {
22
29
  ActionState,
23
30
  } from '@finos/legend-shared';
24
31
  import {
32
+ TerminalItemType,
25
33
  type CartItem,
26
34
  type CartItemRequest,
27
35
  type CartItemResponse,
@@ -64,8 +72,10 @@ export class CartStore {
64
72
  businessReason: observable,
65
73
  open: observable,
66
74
  cartSummary: observable,
75
+ cartUser: computed,
76
+ cartItemIds: computed,
67
77
  setOpen: action,
68
- setTargetUser: action,
78
+ setTargetUser: flow,
69
79
  setBusinessReason: action,
70
80
  initialize: flow,
71
81
  submitOrder: flow,
@@ -81,12 +91,50 @@ export class CartStore {
81
91
  return this.baseStore.applicationStore.identityService.currentUser;
82
92
  }
83
93
 
94
+ get cartUser(): string {
95
+ return this.targetUser ?? this.currentUser;
96
+ }
97
+
98
+ get cartItemIds(): Set<number> {
99
+ const ids = new Set<number>();
100
+ for (const vendorProfileId in this.items) {
101
+ if (Object.prototype.hasOwnProperty.call(this.items, vendorProfileId)) {
102
+ const cartItems = this.items[Number(vendorProfileId)];
103
+ if (cartItems) {
104
+ for (const item of cartItems) {
105
+ ids.add(item.id);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return ids;
111
+ }
112
+
84
113
  setOpen(val: boolean): void {
85
114
  this.open = val;
86
115
  }
87
116
 
88
- setTargetUser(val: string | undefined): void {
117
+ *setTargetUser(val: string | undefined): GeneratorFn<void> {
118
+ this.loadingState.inProgress();
89
119
  this.targetUser = val;
120
+ this.items = {};
121
+ this.cartSummary = {
122
+ total_items: 0,
123
+ total_cost: 0,
124
+ formatted_total_cost: '$0.00',
125
+ };
126
+ this.businessReason = undefined;
127
+ try {
128
+ yield flowResult(this.refresh());
129
+ this.loadingState.complete();
130
+ } catch (error) {
131
+ assertErrorThrown(error);
132
+ this.baseStore.applicationStore.logService.error(
133
+ LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE),
134
+ `Failed to load cart for user: ${error.message}`,
135
+ );
136
+ this.loadingState.fail();
137
+ }
90
138
  }
91
139
 
92
140
  setBusinessReason(val: string | undefined): void {
@@ -94,23 +142,39 @@ export class CartStore {
94
142
  }
95
143
 
96
144
  isItemInCart(itemId: number): boolean {
145
+ return this.cartItemIds.has(itemId);
146
+ }
147
+
148
+ /**
149
+ * Returns the add-on items that depend on the given cart item.
150
+ * When a Terminal is deleted, its associated add-ons (same vendor) must also be removed.
151
+ */
152
+ getDependentAddOns(cartId: number): CartItem[] {
97
153
  for (const vendorProfileId in this.items) {
98
154
  if (Object.prototype.hasOwnProperty.call(this.items, vendorProfileId)) {
99
155
  const cartItems = this.items[Number(vendorProfileId)];
100
- if (cartItems?.some((item) => item.id === itemId)) {
101
- return true;
156
+ if (cartItems) {
157
+ const target = cartItems.find((item) => item.cartId === cartId);
158
+ if (target && target.category === TerminalItemType.TERMINAL) {
159
+ return cartItems.filter(
160
+ (item) =>
161
+ item.cartId !== cartId &&
162
+ item.category === TerminalItemType.ADD_ON,
163
+ );
164
+ }
102
165
  }
103
166
  }
104
167
  }
105
- return false;
168
+ return [];
106
169
  }
107
170
 
108
171
  *addToCartWithAPI(cartItemData: CartItemRequest): GeneratorFn<{
109
172
  success: boolean;
110
173
  recommendations?: TerminalResult[];
111
174
  message: string;
175
+ totalCount?: number | null;
112
176
  }> {
113
- const user = this.currentUser;
177
+ const user = this.cartUser;
114
178
 
115
179
  if (!user) {
116
180
  const message = 'User not authenticated';
@@ -125,11 +189,6 @@ export class CartStore {
125
189
  cartItemData,
126
190
  )) as CartItemResponse;
127
191
 
128
- this.cartSummary =
129
- (yield this.baseStore.marketplaceServerClient.getCartSummary(
130
- user,
131
- )) as CartSummary;
132
-
133
192
  yield flowResult(this.refresh());
134
193
 
135
194
  const responseMessage: string = response.message;
@@ -159,6 +218,7 @@ export class CartStore {
159
218
  success: true,
160
219
  recommendations,
161
220
  message: responseMessage,
221
+ totalCount: response.total_count,
162
222
  };
163
223
  } catch (error) {
164
224
  assertErrorThrown(error);
@@ -195,7 +255,7 @@ export class CartStore {
195
255
  }
196
256
  this.initState.inProgress();
197
257
  try {
198
- this.refresh();
258
+ yield flowResult(this.refresh());
199
259
  this.initState.complete();
200
260
  } catch (error) {
201
261
  assertErrorThrown(error);
@@ -208,7 +268,7 @@ export class CartStore {
208
268
  }
209
269
 
210
270
  *refresh(): GeneratorFn<void> {
211
- const user = this.currentUser;
271
+ const user = this.cartUser;
212
272
  if (!user) {
213
273
  return;
214
274
  }
@@ -231,31 +291,6 @@ export class CartStore {
231
291
  }
232
292
  }
233
293
 
234
- *getCartSummary(): GeneratorFn<void> {
235
- const user = this.currentUser;
236
- if (!user) {
237
- return;
238
- }
239
- try {
240
- const cartSummary =
241
- (yield this.baseStore.marketplaceServerClient.getCartSummary(
242
- user,
243
- )) as CartSummary;
244
- this.cartSummary = cartSummary;
245
- } catch (error) {
246
- assertErrorThrown(error);
247
- this.cartSummary = {
248
- total_items: 0,
249
- total_cost: 0,
250
- formatted_total_cost: '$0.00',
251
- };
252
- this.baseStore.applicationStore.logService.error(
253
- LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE),
254
- `Failed to get cart summary: ${error.message}`,
255
- );
256
- }
257
- }
258
-
259
294
  *submitOrder(): GeneratorFn<void> {
260
295
  if (!this.businessReason) {
261
296
  toastManager.warning(
@@ -277,18 +312,16 @@ export class CartStore {
277
312
  try {
278
313
  const orderData: OrderDetails = {
279
314
  ordered_by: user,
280
- kerberos: this.targetUser ?? user,
315
+ kerberos: this.cartUser,
281
316
  order_items: this.items,
282
317
  business_justification: this.businessReason,
283
318
  };
284
319
 
285
320
  yield this.baseStore.marketplaceServerClient.submitOrder(user, orderData);
286
321
 
287
- this.getCartSummary();
288
-
289
322
  toastManager.notify('Order created successfully!', 'success');
290
323
 
291
- this.refresh();
324
+ yield flowResult(this.refresh());
292
325
  this.setBusinessReason(undefined);
293
326
  this.open = false;
294
327
  this.submitState.complete();
@@ -301,7 +334,7 @@ export class CartStore {
301
334
  }
302
335
 
303
336
  *clearCart(): GeneratorFn<void> {
304
- const user = this.currentUser;
337
+ const user = this.cartUser;
305
338
  if (!user) {
306
339
  toastManager.error('User not authenticated');
307
340
  return;
@@ -310,8 +343,7 @@ export class CartStore {
310
343
  this.loadingState.inProgress();
311
344
  try {
312
345
  yield this.baseStore.marketplaceServerClient.clearCart(user);
313
- this.refresh();
314
- this.getCartSummary();
346
+ yield flowResult(this.refresh());
315
347
  toastManager.success('Cart cleared successfully');
316
348
  this.loadingState.complete();
317
349
  } catch (error) {
@@ -322,8 +354,8 @@ export class CartStore {
322
354
  }
323
355
  }
324
356
 
325
- *deleteCartItem(cartId: number): GeneratorFn<void> {
326
- const user = this.currentUser;
357
+ *deleteCartItem(cartId: number, confirmDelete?: boolean): GeneratorFn<void> {
358
+ const user = this.cartUser;
327
359
  if (!user) {
328
360
  toastManager.error('User not authenticated');
329
361
  return;
@@ -331,10 +363,13 @@ export class CartStore {
331
363
 
332
364
  this.loadingState.inProgress();
333
365
  try {
334
- yield this.baseStore.marketplaceServerClient.deleteCartItem(user, cartId);
366
+ yield this.baseStore.marketplaceServerClient.deleteCartItem(
367
+ user,
368
+ cartId,
369
+ confirmDelete,
370
+ );
335
371
 
336
- this.getCartSummary();
337
- this.refresh();
372
+ yield flowResult(this.refresh());
338
373
  toastManager.success('Item removed successfully');
339
374
  this.loadingState.complete();
340
375
  } catch (error) {
@@ -19,6 +19,7 @@ import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.j
19
19
  import {
20
20
  ActionState,
21
21
  assertErrorThrown,
22
+ isString,
22
23
  type GeneratorFn,
23
24
  } from '@finos/legend-shared';
24
25
  import {
@@ -38,6 +39,7 @@ export enum LegendServiceSort {
38
39
  export enum ServicesViewMode {
39
40
  LIST = 'list',
40
41
  TILE = 'tile',
42
+ GRID = 'grid',
41
43
  }
42
44
 
43
45
  export class LegendServiceCardState {
@@ -119,6 +121,10 @@ const LEGEND_MARKETPLACE_SETTING_KEY_SERVICES_VIEW_MODE =
119
121
  'marketplace.data-apis.viewMode';
120
122
  const LEGEND_MARKETPLACE_SETTING_KEY_SHOW_OWN_SERVICES =
121
123
  'marketplace.data-apis.showOwnServicesOnly';
124
+ const LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES =
125
+ 'marketplace.data-apis.favorites';
126
+ const LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE =
127
+ 'marketplace.data-apis.itemsPerPage';
122
128
 
123
129
  export class LegendMarketplaceDataAPIsStore {
124
130
  readonly marketplaceBaseStore: LegendMarketplaceBaseStore;
@@ -127,12 +133,15 @@ export class LegendMarketplaceDataAPIsStore {
127
133
  sort: LegendServiceSort = LegendServiceSort.DEFAULT;
128
134
  viewMode: ServicesViewMode;
129
135
  showOwnServicesOnly: boolean;
136
+ showFavoritesOnly = false;
137
+ favoritePatterns: Set<string>;
130
138
  serviceCardStates: LegendServiceCardState[] = [];
131
139
  page = 1;
132
- itemsPerPage = 12;
140
+ itemsPerPage: number;
133
141
 
134
142
  ownerFilters: string[] = [];
135
143
  deploymentIdFilters: string[] = [];
144
+ favorites: Set<string> = new Set();
136
145
 
137
146
  readonly fetchingServicesState = ActionState.create();
138
147
 
@@ -164,20 +173,37 @@ export class LegendMarketplaceDataAPIsStore {
164
173
  );
165
174
  this.showOwnServicesOnly = persistedShowOwn ?? false;
166
175
 
176
+ const persistedFavorites =
177
+ (this.marketplaceBaseStore.applicationStore.settingService.getObjectValue(
178
+ LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES,
179
+ ) as string[] | undefined) ?? [];
180
+ this.favoritePatterns = new Set(persistedFavorites.filter(isString));
181
+
182
+ const persistedItemsPerPage =
183
+ this.marketplaceBaseStore.applicationStore.settingService.getNumericValue(
184
+ LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE,
185
+ );
186
+ this.itemsPerPage = persistedItemsPerPage ?? 12;
187
+
167
188
  makeObservable(this, {
168
189
  searchQuery: observable,
169
190
  sort: observable,
170
191
  viewMode: observable,
171
192
  showOwnServicesOnly: observable,
193
+ showFavoritesOnly: observable,
194
+ favoritePatterns: observable.shallow,
172
195
  serviceCardStates: observable,
173
196
  page: observable,
174
197
  itemsPerPage: observable,
175
198
  ownerFilters: observable,
176
199
  deploymentIdFilters: observable,
200
+ favorites: observable,
177
201
  setSearchQuery: action,
178
202
  setSort: action,
179
203
  setViewMode: action,
180
204
  setShowOwnServicesOnly: action,
205
+ setShowFavoritesOnly: action,
206
+ toggleFavorite: action,
181
207
  setPage: action,
182
208
  setItemsPerPage: action,
183
209
  addOwnerFilter: action,
@@ -220,13 +246,37 @@ export class LegendMarketplaceDataAPIsStore {
220
246
  );
221
247
  }
222
248
 
249
+ setShowFavoritesOnly(value: boolean): void {
250
+ this.showFavoritesOnly = value;
251
+ this.page = 1;
252
+ }
253
+
254
+ isFavorite(pattern: string): boolean {
255
+ return this.favoritePatterns.has(pattern);
256
+ }
257
+
258
+ toggleFavorite(pattern: string): void {
259
+ if (this.favoritePatterns.has(pattern)) {
260
+ this.favoritePatterns.delete(pattern);
261
+ } else {
262
+ this.favoritePatterns.add(pattern);
263
+ }
264
+ this.marketplaceBaseStore.applicationStore.settingService.persistValue(
265
+ LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES,
266
+ Array.from(this.favoritePatterns),
267
+ );
268
+ }
269
+
223
270
  setPage(value: number): void {
224
271
  this.page = value;
225
272
  }
226
273
 
227
274
  setItemsPerPage(value: number): void {
228
275
  this.itemsPerPage = value;
229
- this.page = 1;
276
+ this.marketplaceBaseStore.applicationStore.settingService.persistValue(
277
+ LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE,
278
+ value,
279
+ );
230
280
  }
231
281
 
232
282
  private persistOwnerFilters(): void {
@@ -320,6 +370,12 @@ export class LegendMarketplaceDataAPIsStore {
320
370
  get filteredSortedServices(): LegendServiceCardState[] {
321
371
  let results = this.serviceCardStates;
322
372
 
373
+ if (this.showFavoritesOnly) {
374
+ results = results.filter((card) =>
375
+ this.favoritePatterns.has(card.service.pattern),
376
+ );
377
+ }
378
+
323
379
  if (this.showOwnServicesOnly) {
324
380
  const currentUser =
325
381
  this.marketplaceBaseStore.applicationStore.identityService.currentUser;
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { action, computed, flow, makeObservable, observable } from 'mobx';
18
+ import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.js';
19
+ import {
20
+ ActionState,
21
+ assertErrorThrown,
22
+ isNonEmptyString,
23
+ LogEvent,
24
+ type GeneratorFn,
25
+ type PlainObject,
26
+ } from '@finos/legend-shared';
27
+ import {
28
+ FieldSearchType,
29
+ type FieldSearchRequest,
30
+ GroupedFieldSearchResponse,
31
+ type GroupedFieldSearchResponseMetadata,
32
+ } from '@finos/legend-server-marketplace';
33
+ import type { DataProductTypeFilter } from './LegendMarketplaceSearchResultsStore.js';
34
+ import { LEGEND_MARKETPLACE_APP_EVENT } from '../../__lib__/LegendMarketplaceAppEvent.js';
35
+ import { LegendMarketplaceTelemetryHelper } from '../../__lib__/LegendMarketplaceTelemetryHelper.js';
36
+ import { FieldSearchResultState } from './fieldSearch/FieldSearchResultState.js';
37
+
38
+ export class LegendMarketplaceFieldSearchResultsStore {
39
+ readonly marketplaceBaseStore: LegendMarketplaceBaseStore;
40
+ searchQuery: string | undefined = undefined;
41
+ fieldSearchResultStates: FieldSearchResultState[] = [];
42
+ responseMetadata: GroupedFieldSearchResponseMetadata | undefined = undefined;
43
+ page = 1;
44
+ itemsPerPage = 12;
45
+ errorMessage: string | undefined = undefined;
46
+ selectedProductTypes: Set<DataProductTypeFilter> =
47
+ new Set<DataProductTypeFilter>();
48
+ expandedRows: Set<string> = new Set<string>();
49
+ readonly loadState = ActionState.create();
50
+ private _currentFetchToken = 0;
51
+ private _abortController: AbortController | undefined = undefined;
52
+
53
+ constructor(marketplaceBaseStore: LegendMarketplaceBaseStore) {
54
+ this.marketplaceBaseStore = marketplaceBaseStore;
55
+
56
+ makeObservable<
57
+ LegendMarketplaceFieldSearchResultsStore,
58
+ '_currentFetchToken' | '_abortController'
59
+ >(this, {
60
+ searchQuery: observable,
61
+ fieldSearchResultStates: observable,
62
+ responseMetadata: observable,
63
+ page: observable,
64
+ itemsPerPage: observable,
65
+ errorMessage: observable,
66
+ selectedProductTypes: observable,
67
+ expandedRows: observable,
68
+ _currentFetchToken: false,
69
+ _abortController: false,
70
+ setSearchQuery: action,
71
+ setFieldSearchResultStates: action,
72
+ setResponseMetadata: action,
73
+ setPage: action,
74
+ setItemsPerPage: action,
75
+ setErrorMessage: action,
76
+ toggleProductType: action,
77
+ clearAllFilters: action,
78
+ toggleExpandRow: action,
79
+ clearExpandedRows: action,
80
+ fieldSearchRequest: computed,
81
+ tableRows: computed,
82
+ totalItems: computed,
83
+ totalFieldMatches: computed,
84
+ lakehouseCount: computed,
85
+ legacyCount: computed,
86
+ hasActiveFilters: computed,
87
+ isLoading: computed,
88
+ hasFailed: computed,
89
+ executeSearch: flow,
90
+ changePageAndFetch: flow,
91
+ changeItemsPerPageAndFetch: flow,
92
+ toggleProductTypeAndFetch: flow,
93
+ clearAllFiltersAndFetch: flow,
94
+ });
95
+ }
96
+
97
+ setSearchQuery(query: string | undefined): void {
98
+ this.searchQuery = query;
99
+ this.page = 1;
100
+ }
101
+
102
+ setFieldSearchResultStates(results: FieldSearchResultState[]): void {
103
+ this.fieldSearchResultStates = results;
104
+ }
105
+
106
+ setResponseMetadata(
107
+ value: GroupedFieldSearchResponseMetadata | undefined,
108
+ ): void {
109
+ this.responseMetadata = value;
110
+ }
111
+
112
+ setPage(value: number): void {
113
+ this.page = value;
114
+ }
115
+
116
+ setItemsPerPage(value: number): void {
117
+ this.itemsPerPage = value;
118
+ this.page = 1;
119
+ }
120
+
121
+ setErrorMessage(value: string | undefined): void {
122
+ this.errorMessage = value;
123
+ }
124
+
125
+ toggleProductType(productType: DataProductTypeFilter): void {
126
+ const wasSelected = this.selectedProductTypes.has(productType);
127
+ if (wasSelected) {
128
+ this.selectedProductTypes.delete(productType);
129
+ } else {
130
+ this.selectedProductTypes.add(productType);
131
+ }
132
+ this.page = 1;
133
+ LegendMarketplaceTelemetryHelper.logEvent_ApplySearchFilter(
134
+ this.marketplaceBaseStore.applicationStore.telemetryService,
135
+ 'dataProductType',
136
+ productType,
137
+ wasSelected ? 'deselect' : 'select',
138
+ this.searchQuery,
139
+ );
140
+ }
141
+
142
+ clearAllFilters(): void {
143
+ this.selectedProductTypes.clear();
144
+ this.page = 1;
145
+ LegendMarketplaceTelemetryHelper.logEvent_ClearSearchFilters(
146
+ this.marketplaceBaseStore.applicationStore.telemetryService,
147
+ this.searchQuery,
148
+ );
149
+ }
150
+
151
+ toggleExpandRow(rowId: string): void {
152
+ if (this.expandedRows.has(rowId)) {
153
+ this.expandedRows.delete(rowId);
154
+ } else {
155
+ this.expandedRows.add(rowId);
156
+ }
157
+ }
158
+
159
+ clearExpandedRows(): void {
160
+ this.expandedRows.clear();
161
+ }
162
+
163
+ isRowExpanded(rowId: string): boolean {
164
+ return this.expandedRows.has(rowId);
165
+ }
166
+
167
+ get fieldSearchRequest(): FieldSearchRequest | undefined {
168
+ if (!isNonEmptyString(this.searchQuery)) {
169
+ return undefined;
170
+ }
171
+ return {
172
+ query: this.searchQuery,
173
+ searchType: FieldSearchType.HYBRID,
174
+ pageSize: this.itemsPerPage,
175
+ pageNumber: this.page,
176
+ ...(this.selectedProductTypes.size > 0
177
+ ? { dataProductTypes: Array.from(this.selectedProductTypes) }
178
+ : {}),
179
+ };
180
+ }
181
+
182
+ get tableRows(): FieldSearchResultState[] {
183
+ return this.fieldSearchResultStates;
184
+ }
185
+
186
+ get totalItems(): number {
187
+ return this.responseMetadata?.total_count ?? 0;
188
+ }
189
+
190
+ get totalFieldMatches(): number {
191
+ return this.responseMetadata?.total_field_matches ?? 0;
192
+ }
193
+
194
+ get lakehouseCount(): number {
195
+ return this.responseMetadata?.lakehouse_count ?? 0;
196
+ }
197
+
198
+ get legacyCount(): number {
199
+ return this.responseMetadata?.legacy_count ?? 0;
200
+ }
201
+
202
+ get hasActiveFilters(): boolean {
203
+ return this.selectedProductTypes.size > 0;
204
+ }
205
+
206
+ get isLoading(): boolean {
207
+ return (
208
+ isNonEmptyString(this.searchQuery) &&
209
+ (this.loadState.isInInitialState || this.loadState.isInProgress)
210
+ );
211
+ }
212
+
213
+ get hasFailed(): boolean {
214
+ return this.loadState.hasFailed;
215
+ }
216
+
217
+ *executeSearch(): GeneratorFn<void> {
218
+ yield* this.fetchFieldSearchResultsInternal();
219
+ }
220
+
221
+ *changePageAndFetch(page: number): GeneratorFn<void> {
222
+ this.setPage(page);
223
+ yield* this.fetchFieldSearchResultsInternal();
224
+ }
225
+
226
+ *changeItemsPerPageAndFetch(itemsPerPage: number): GeneratorFn<void> {
227
+ this.setItemsPerPage(itemsPerPage);
228
+ yield* this.fetchFieldSearchResultsInternal();
229
+ }
230
+
231
+ *toggleProductTypeAndFetch(
232
+ productType: DataProductTypeFilter,
233
+ ): GeneratorFn<void> {
234
+ this.toggleProductType(productType);
235
+ yield* this.fetchFieldSearchResultsInternal();
236
+ }
237
+
238
+ *clearAllFiltersAndFetch(): GeneratorFn<void> {
239
+ this.clearAllFilters();
240
+ yield* this.fetchFieldSearchResultsInternal();
241
+ }
242
+
243
+ private *fetchFieldSearchResultsInternal(): GeneratorFn<void> {
244
+ const request = this.fieldSearchRequest;
245
+ const fetchToken = ++this._currentFetchToken;
246
+
247
+ if (!request) {
248
+ this.setFieldSearchResultStates([]);
249
+ this.setResponseMetadata(undefined);
250
+ this.setErrorMessage(undefined);
251
+ this.clearExpandedRows();
252
+ this.loadState.complete();
253
+ return;
254
+ }
255
+
256
+ this._abortController?.abort();
257
+ this._abortController = new AbortController();
258
+ const { signal } = this._abortController;
259
+
260
+ this.loadState.inProgress();
261
+ this.setErrorMessage(undefined);
262
+ this.clearExpandedRows();
263
+
264
+ try {
265
+ const rawResponse =
266
+ (yield this.marketplaceBaseStore.marketplaceServerClient.fieldSearch(
267
+ this.marketplaceBaseStore.envState.lakehouseEnvironment,
268
+ request,
269
+ signal,
270
+ )) as PlainObject<GroupedFieldSearchResponse>;
271
+
272
+ if (fetchToken !== this._currentFetchToken) {
273
+ return;
274
+ }
275
+
276
+ const response =
277
+ GroupedFieldSearchResponse.serialization.fromJson(rawResponse);
278
+ const results = response.results.map(
279
+ (entry) => new FieldSearchResultState(entry),
280
+ );
281
+
282
+ this.setFieldSearchResultStates(results);
283
+ this.setResponseMetadata(response.metadata);
284
+ this.loadState.complete();
285
+ } catch (error: unknown) {
286
+ if (fetchToken !== this._currentFetchToken) {
287
+ return;
288
+ }
289
+
290
+ assertErrorThrown(error);
291
+ this.marketplaceBaseStore.applicationStore.logService.error(
292
+ LogEvent.create(LEGEND_MARKETPLACE_APP_EVENT.FIELD_SEARCH_FAILURE),
293
+ error,
294
+ );
295
+ this.marketplaceBaseStore.applicationStore.notificationService.notifyError(
296
+ `Error fetching field search results: ${error.message}`,
297
+ );
298
+ this.setFieldSearchResultStates([]);
299
+ this.setResponseMetadata(undefined);
300
+ this.setErrorMessage(error.message);
301
+ this.loadState.fail();
302
+ }
303
+ }
304
+
305
+ dispose(): void {
306
+ this._abortController?.abort();
307
+ this._abortController = undefined;
308
+ }
309
+ }