@startinblox/components-ds4go 3.3.8 → 4.1.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 (31) hide show
  1. package/dist/components-ds4go.css +1 -1
  2. package/dist/index.js +3534 -3602
  3. package/locales/en.xlf +187 -3
  4. package/package.json +2 -2
  5. package/src/component.d.ts +0 -5
  6. package/src/components/cards/ds4go-card-dataspace-catalog.ts +82 -227
  7. package/src/components/cards/ds4go-card-fact.ts +128 -0
  8. package/src/components/catalog/ds4go-catalog-data-holder.ts +158 -0
  9. package/src/components/catalog/ds4go-fact-holder.ts +149 -0
  10. package/src/components/modal/catalog-modal/agreement-info.ts +110 -0
  11. package/src/components/modal/catalog-modal/index.ts +4 -0
  12. package/src/components/modal/catalog-modal/negotiation-button.ts +111 -0
  13. package/src/components/modal/catalog-modal/policy-display.ts +66 -0
  14. package/src/components/modal/catalog-modal/policy-selection.ts +71 -0
  15. package/src/components/modal/ds4go-catalog-modal.ts +158 -1105
  16. package/src/components/modal/ds4go-fact-modal.ts +217 -0
  17. package/src/components/odrl/policy-composer.ts +1 -1
  18. package/src/components/odrl-policy-viewer.ts +0 -21
  19. package/src/components/solid-dsp-catalog.ts +2 -43
  20. package/src/components/solid-fact-list.ts +307 -0
  21. package/src/ds4go.d.ts +78 -1
  22. package/src/helpers/dsp/agreementStorage.ts +243 -0
  23. package/src/helpers/dsp/policyHelpers.ts +223 -0
  24. package/src/helpers/index.ts +7 -0
  25. package/src/styles/cards/ds4go-card-catalog.scss +1 -1
  26. package/src/styles/cards/ds4go-card-dataspace-catalog.scss +22 -165
  27. package/src/styles/cards/ds4go-card-fact.scss +112 -0
  28. package/src/styles/index.scss +42 -0
  29. package/src/styles/modal/ds4go-catalog-modal.scss +1 -1
  30. package/src/styles/modal/ds4go-fact-modal.scss +161 -0
  31. package/src/components/modal/ds4go-catalog-data-holder.ts +0 -349
@@ -1,18 +1,10 @@
1
- import { formatDate } from "@helpers";
1
+ import { dspAgreementStorage, dspPolicyHelpers } from "@helpers";
2
2
  import { localized, msg } from "@lit/localize";
3
- import type { TemplateResultOrSymbol } from "@src/component";
4
- import {
5
- DSPContractStorage,
6
- offerKindActionHandler,
7
- offerKindHandler,
8
- rdf,
9
- TemsObjectHandler,
10
- } from "@startinblox/solid-tems-shared";
3
+ import { TemsObjectHandler } from "@startinblox/solid-tems-shared";
11
4
  import ModalStyle from "@styles/modal/ds4go-catalog-modal.scss?inline";
12
- import { css, html, nothing, type TemplateResult, unsafeCSS } from "lit";
5
+ import type { DSPOffer, ODRLPolicy } from "@src/ds4go";
6
+ import { css, html, nothing, unsafeCSS } from "lit";
13
7
  import { customElement, property, state } from "lit/decorators.js";
14
- import { ifDefined } from "lit/directives/if-defined.js";
15
- import { unsafeHTML } from "lit/directives/unsafe-html.js";
16
8
 
17
9
  @customElement("ds4go-catalog-modal")
18
10
  @localized()
@@ -27,6 +19,18 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
27
19
  @property({ attribute: false })
28
20
  participantId?: string;
29
21
 
22
+ @property({ attribute: false })
23
+ counterPartyId?: string;
24
+
25
+ @property({ attribute: false })
26
+ counterPartyAddress?: string;
27
+
28
+ @property({ attribute: false })
29
+ providerName?: string;
30
+
31
+ @property({ attribute: false })
32
+ providerColor?: string;
33
+
30
34
  @state()
31
35
  negotiationStatus:
32
36
  | "idle"
@@ -64,8 +68,7 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
64
68
  selectedPolicyIndex?: number;
65
69
 
66
70
  @state()
67
- availablePolicies?: any[];
68
-
71
+ availablePolicies?: ODRLPolicy[];
69
72
 
70
73
  /**
71
74
  * Check for existing agreement when component connects
@@ -75,235 +78,6 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
75
78
  this._checkExistingAgreement();
76
79
  }
77
80
 
78
- /**
79
- * Get localStorage key for this asset
80
- * Uses combination of provider ID and dataset ID for uniqueness across providers
81
- */
82
- private _getStorageKey(): string {
83
- const obj = this.object as any;
84
- const datasetId = obj.datasetId || obj.assetId;
85
- // Include provider ID to differentiate assets with same ID from different providers
86
- const providerId =
87
- obj.counterPartyId || obj._providerParticipantId || obj._provider || "";
88
-
89
- // DEBUG: Log what provider info we're seeing
90
- if (!datasetId) return "";
91
- // Create composite key: provider-assetId
92
- const key = providerId
93
- ? `dsp-agreement-${providerId}-${datasetId}`
94
- : `dsp-agreement-${datasetId}`;
95
- return key;
96
- }
97
-
98
- /**
99
- * Save agreement info to localStorage
100
- */
101
- private _saveAgreementInfo(
102
- contractId: string,
103
- negotiationId: string,
104
- timestamp: number,
105
- ) {
106
- const key = this._getStorageKey();
107
- if (key) {
108
- const obj = this.object as any;
109
- const agreementInfo = {
110
- contractId,
111
- negotiationId,
112
- timestamp,
113
- assetId: obj.datasetId || obj.assetId,
114
- providerId:
115
- obj.counterPartyId ||
116
- obj._providerParticipantId ||
117
- obj._provider ||
118
- "",
119
- providerAddress: obj.counterPartyAddress || obj._providerAddress || "",
120
- };
121
- localStorage.setItem(key, JSON.stringify(agreementInfo));
122
- }
123
- }
124
-
125
- /**
126
- * Load agreement info from localStorage
127
- */
128
- private _loadAgreementInfo(): {
129
- contractId: string;
130
- negotiationId: string;
131
- timestamp: number;
132
- assetId: string;
133
- } | null {
134
- const key = this._getStorageKey();
135
- if (!key) return null;
136
-
137
- try {
138
- const stored = localStorage.getItem(key);
139
- if (stored) {
140
- const info = JSON.parse(stored);
141
- return info;
142
- }
143
- } catch (error) {
144
- console.error("Failed to load agreement info:", error);
145
- }
146
- return null;
147
- }
148
-
149
- /**
150
- * Save initial contract state when negotiation starts
151
- */
152
- private _saveInitialContractState(negotiationId: string) {
153
- try {
154
- const obj = this.object as any;
155
-
156
- // Check if contract already exists for this asset from this provider
157
- const providerId = obj.counterPartyId || obj._providerParticipantId || "";
158
- const existingContracts = DSPContractStorage.getByAssetAndProvider(
159
- obj.assetId || obj.datasetId,
160
- providerId,
161
- );
162
- const existingContract = existingContracts.find(
163
- (c) => c.contractId === negotiationId,
164
- );
165
-
166
- if (!existingContract) {
167
- // Debug: log asset object to see available properties
168
-
169
- // Extract index endpoint URL from asset (dcat:endpointUrl)
170
- const indexEndpointUrl =
171
- obj["dcat:endpointUrl"] || obj.endpointUrl || obj["endpointUrl"];
172
-
173
- const assetName = obj.name || obj.assetId || "Unknown Asset";
174
-
175
- // Detect if this is an index asset
176
- const isIndexAsset = assetName.toLowerCase().includes("index");
177
-
178
- // Create new contract in REQUESTED state
179
- DSPContractStorage.create({
180
- assetId: obj.assetId || obj.datasetId,
181
- datasetId: obj.datasetId || obj.assetId,
182
- assetName,
183
- assetDescription: obj.description,
184
- providerName:
185
- obj._provider || obj.provider?.name || "Unknown Provider",
186
- providerAddress:
187
- obj.counterPartyAddress || obj._providerAddress || "",
188
- providerParticipantId:
189
- obj.counterPartyId || obj._providerParticipantId || "",
190
- providerColor: obj._providerColor,
191
- policy: obj.policy,
192
- state: "REQUESTED",
193
- contractId: negotiationId,
194
- // Index-specific fields
195
- isIndexAsset,
196
- indexEndpointUrl,
197
- });
198
- }
199
- } catch (error) {
200
- console.error(
201
- "[DSP Contract Catalog] Failed to save initial contract state:",
202
- error,
203
- );
204
- }
205
- }
206
-
207
- /**
208
- * Save contract to DSP Contract Catalog for history tracking
209
- */
210
- private _saveToContractCatalog(contractId: string, negotiationId: string) {
211
- try {
212
- const obj = this.object as any;
213
-
214
- // Debug: log asset object to see available properties for endpoint URL
215
-
216
- // Check if contract already exists - search by contractId (negotiationId) first,
217
- // then by agreementId as fallback. Filter by provider to avoid cross-provider confusion.
218
- const providerId = obj.counterPartyId || obj._providerParticipantId || "";
219
- const existingContracts = DSPContractStorage.getByAssetAndProvider(
220
- obj.assetId || obj.datasetId,
221
- providerId,
222
- );
223
- const existingContract = existingContracts.find(
224
- (c) => c.contractId === negotiationId || c.agreementId === contractId,
225
- );
226
-
227
- // Extract index endpoint URL from asset (dcat:endpointUrl)
228
- // Try multiple possible property names - the mapping config adds 'indexEndpointUrl'
229
- const indexEndpointUrl =
230
- obj.indexEndpointUrl ||
231
- obj["dcat:endpointUrl"] ||
232
- obj["dcat:endpointURL"] ||
233
- obj.endpointUrl ||
234
- obj["endpointUrl"] ||
235
- obj.endpointURL;
236
- const assetName = obj.name || obj.assetId || "Unknown Asset";
237
-
238
- // Detect if this is an index asset
239
- const isIndexAsset = assetName.toLowerCase().includes("index");
240
-
241
- if (existingContract) {
242
- // Update existing contract with index metadata
243
- DSPContractStorage.updateState(existingContract.id, "FINALIZED", {
244
- agreementId: contractId,
245
- contractId: negotiationId,
246
- isIndexAsset,
247
- indexEndpointUrl,
248
- });
249
- } else {
250
- // Create new contract entry
251
- DSPContractStorage.create({
252
- assetId: obj.assetId || obj.datasetId,
253
- datasetId: obj.datasetId || obj.assetId,
254
- assetName,
255
- assetDescription: obj.description,
256
- providerName:
257
- obj._provider || obj.provider?.name || "Unknown Provider",
258
- providerAddress:
259
- obj.counterPartyAddress || obj._providerAddress || "",
260
- providerParticipantId:
261
- obj.counterPartyId || obj._providerParticipantId || "",
262
- providerColor: obj._providerColor,
263
- policy: obj.policy,
264
- state: "FINALIZED",
265
- contractId: negotiationId,
266
- agreementId: contractId,
267
- // Index-specific fields
268
- isIndexAsset,
269
- indexEndpointUrl,
270
- });
271
- }
272
- } catch (error) {
273
- console.error("[DSP Contract Catalog] Failed to save contract:", error);
274
- }
275
- }
276
-
277
- /**
278
- * Update contract state in catalog (for failures)
279
- */
280
- private _updateContractState(
281
- negotiationId: string,
282
- state: "FAILED" | "TERMINATED",
283
- error?: string,
284
- ) {
285
- try {
286
- const obj = this.object as any;
287
- const providerId = obj.counterPartyId || obj._providerParticipantId || "";
288
- const existingContracts = DSPContractStorage.getByAssetAndProvider(
289
- obj.assetId || obj.datasetId,
290
- providerId,
291
- );
292
- const existingContract = existingContracts.find(
293
- (c) => c.contractId === negotiationId,
294
- );
295
-
296
- if (existingContract) {
297
- DSPContractStorage.updateState(existingContract.id, state, { error });
298
- }
299
- } catch (error) {
300
- console.error(
301
- "[DSP Contract Catalog] Failed to update contract state:",
302
- error,
303
- );
304
- }
305
- }
306
-
307
81
  /**
308
82
  * Check if there's an existing agreement for this asset
309
83
  */
@@ -311,8 +85,10 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
311
85
  if (this.existingAgreementChecked) return;
312
86
  this.existingAgreementChecked = true;
313
87
 
88
+ const offer = this.object as DSPOffer;
89
+
314
90
  // Try to load from localStorage
315
- const storedInfo = this._loadAgreementInfo();
91
+ const storedInfo = dspAgreementStorage.loadAgreementInfo(offer);
316
92
  if (storedInfo) {
317
93
  this.contractId = storedInfo.contractId;
318
94
  this.negotiationId = storedInfo.negotiationId;
@@ -323,24 +99,18 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
323
99
  // Also check if DSP store has the agreement
324
100
  try {
325
101
  if (this.dspStore && storedInfo?.negotiationId) {
326
- // Verify the agreement still exists in the store
327
- try {
328
- const obj = this.object as any;
329
- const providerId =
330
- obj.counterPartyId ||
331
- obj._providerParticipantId ||
332
- obj._provider ||
333
- "";
334
- await this.dspStore.getContractAgreement(
335
- storedInfo.negotiationId,
336
- providerId,
337
- );
338
- } catch (error) {
339
- console.warn("Could not verify agreement in store:", error);
340
- }
102
+ const providerId =
103
+ offer._provider?.participantId ||
104
+ this.counterPartyId ||
105
+ this.participantId ||
106
+ "";
107
+ await this.dspStore.getContractAgreement(
108
+ storedInfo.negotiationId,
109
+ providerId,
110
+ );
341
111
  }
342
112
  } catch (error) {
343
- console.warn("Error checking DSP store for existing agreement:", error);
113
+ console.warn("Could not verify agreement in store:", error);
344
114
  }
345
115
  }
346
116
 
@@ -358,26 +128,15 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
358
128
  return;
359
129
  }
360
130
 
361
- const key = this._getStorageKey();
362
- if (key) {
363
- localStorage.removeItem(key);
364
- }
131
+ const offer = this.object as DSPOffer;
365
132
 
366
- // Also delete from DSPContractStorage - only for this provider's contract
367
- const obj = this.object as any;
368
- const assetId = obj?.assetId || obj?.datasetId;
369
- const providerId = obj?.counterPartyId || obj?._providerParticipantId || "";
370
- if (assetId) {
371
- const existingContracts = DSPContractStorage.getByAssetAndProvider(
372
- assetId,
373
- providerId,
374
- );
375
- for (const contract of existingContracts) {
376
- DSPContractStorage.delete(contract.id);
377
- }
378
- }
133
+ // Clear from localStorage
134
+ dspAgreementStorage.clearAgreementInfo(offer);
379
135
 
380
- // Reset state to idle
136
+ // Delete from DSPContractStorage
137
+ dspAgreementStorage.deleteContractsForAsset(offer);
138
+
139
+ // Reset state
381
140
  this.negotiationStatus = "idle";
382
141
  this.contractId = undefined;
383
142
  this.negotiationId = undefined;
@@ -386,7 +145,11 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
386
145
  this.requestUpdate();
387
146
  }
388
147
 
389
- _selectPolicy(index: number) {
148
+ /**
149
+ * Handle policy selection
150
+ */
151
+ private _handlePolicySelected(e: CustomEvent) {
152
+ const { index } = e.detail;
390
153
  this.selectedPolicyIndex = index;
391
154
  this.showPolicySelection = false;
392
155
  this.requestUpdate();
@@ -394,121 +157,21 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
394
157
  this._negotiateAccess();
395
158
  }
396
159
 
397
- _cancelPolicySelection() {
160
+ /**
161
+ * Handle policy selection cancellation
162
+ */
163
+ private _handlePolicyCancel() {
398
164
  this.showPolicySelection = false;
399
165
  this.selectedPolicyIndex = undefined;
400
166
  this.availablePolicies = undefined;
401
167
  this.requestUpdate();
402
168
  }
403
169
 
404
- _formatPolicyDetails(policy: any): string {
405
- if (!policy) return "No policy details available";
406
-
407
- const parts: string[] = [];
408
-
409
- // Policy ID
410
- if (policy["@id"]) {
411
- parts.push(
412
- `<div class="policy-detail"><strong>Policy ID:</strong> ${policy["@id"]}</div>`,
413
- );
414
- }
415
-
416
- // Policy Type
417
- if (policy["@type"]) {
418
- parts.push(
419
- `<div class="policy-detail"><strong>Type:</strong> ${policy["@type"]}</div>`,
420
- );
421
- }
422
-
423
- // Permissions
424
- const permissions = policy["odrl:permission"];
425
- if (permissions) {
426
- const permArray = Array.isArray(permissions)
427
- ? permissions
428
- : [permissions];
429
- if (permArray.length > 0) {
430
- parts.push(
431
- '<div class="policy-detail"><strong>Permissions:</strong><ul>',
432
- );
433
- permArray.forEach((perm: any) => {
434
- const action = perm["odrl:action"];
435
- const actionStr = action?.["@id"] || action || "use";
436
- parts.push(`<li>Action: ${actionStr}</li>`);
437
-
438
- // Constraints
439
- if (perm["odrl:constraint"]) {
440
- const constraints = Array.isArray(perm["odrl:constraint"])
441
- ? perm["odrl:constraint"]
442
- : [perm["odrl:constraint"]];
443
- constraints.forEach((c: any) => {
444
- parts.push(
445
- `<li style="margin-left: 20px;">Constraint: ${c["odrl:leftOperand"]} ${c["odrl:operator"]} ${c["odrl:rightOperand"]}</li>`,
446
- );
447
- });
448
- }
449
- });
450
- parts.push("</ul></div>");
451
- }
452
- }
453
-
454
- // Prohibitions
455
- const prohibitions = policy["odrl:prohibition"];
456
- if (prohibitions) {
457
- const prohibArray = Array.isArray(prohibitions)
458
- ? prohibitions
459
- : [prohibitions];
460
- if (prohibArray.length > 0) {
461
- parts.push(
462
- '<div class="policy-detail"><strong>Prohibitions:</strong><ul>',
463
- );
464
- prohibArray.forEach((prohib: any) => {
465
- const action = prohib["odrl:action"];
466
- const actionStr = action?.["@id"] || action || "unknown";
467
- parts.push(`<li>Action: ${actionStr}</li>`);
468
- });
469
- parts.push("</ul></div>");
470
- }
471
- }
472
-
473
- // Obligations
474
- const obligations = policy["odrl:obligation"];
475
- if (obligations) {
476
- const obligArray = Array.isArray(obligations)
477
- ? obligations
478
- : [obligations];
479
- if (obligArray.length > 0) {
480
- parts.push(
481
- '<div class="policy-detail"><strong>Obligations:</strong><ul>',
482
- );
483
- obligArray.forEach((oblig: any) => {
484
- const action = oblig["odrl:action"];
485
- const actionStr = action?.["@id"] || action || "unknown";
486
- parts.push(`<li>Action: ${actionStr}</li>`);
487
- });
488
- parts.push("</ul></div>");
489
- }
490
- }
491
-
492
- // Target
493
- if (policy.target) {
494
- parts.push(
495
- `<div class="policy-detail"><strong>Target Asset:</strong> ${policy.target}</div>`,
496
- );
497
- }
498
-
499
- // Assigner
500
- if (policy.assigner) {
501
- parts.push(
502
- `<div class="policy-detail"><strong>Assigner:</strong> ${policy.assigner}</div>`,
503
- );
504
- }
505
-
506
- return parts.length > 0 ? parts.join("") : "No policy details available";
507
- }
508
-
170
+ /**
171
+ * Negotiate access to the dataset
172
+ */
509
173
  async _negotiateAccess() {
510
174
  try {
511
- // Use the DSP store passed as property
512
175
  if (!this.dspStore) {
513
176
  throw new Error(
514
177
  "DSP connector not configured. Please provide participant-connector-uri and participant-api-key attributes.",
@@ -516,86 +179,29 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
516
179
  }
517
180
 
518
181
  const dspStore = this.dspStore;
519
-
520
- // DEBUG: Log store configuration to verify correct store is being used
521
- // Use pre-processed contract negotiation fields from the mapped Destination object
522
- // These fields are extracted and processed by FederatedCatalogueStore.mapSourceToDestination()
523
- const obj = this.object as any;
524
- const counterPartyAddress = obj.counterPartyAddress;
525
- const counterPartyId = obj.counterPartyId || this.participantId;
526
- const datasetId = obj.datasetId;
527
-
528
- // DEFENSIVE: Handle case where obj.policy might be an array or have numeric keys
529
- let policies = obj.policies;
530
- let rawPolicy = obj.policy;
531
-
532
- // If obj.policy is an array, convert it to policies array
533
- if (Array.isArray(rawPolicy)) {
534
- console.warn(
535
- "[tems-modal] obj.policy is an array! Converting to policies array.",
536
- );
537
-
538
- // Check if array object has a "target" property
539
- const target = (rawPolicy as any).target;
540
-
541
- // Filter out non-policy properties (like "target")
542
- policies = rawPolicy.filter(
543
- (item: any) => item && typeof item === "object" && item["@id"],
544
- );
545
-
546
- // Add target to each policy if it exists and policy doesn't have one
547
- if (target) {
548
- policies = policies.map((p: any) => {
549
- if (!p.target && !p["odrl:target"]) {
550
- return { ...p, target, "odrl:target": target };
551
- }
552
- return p;
553
- });
554
- }
555
-
556
- rawPolicy = policies.length > 0 ? policies[0] : rawPolicy[0]; // Use first valid policy as default
557
- }
558
- // If obj.policy is an object with numeric keys (array-like object)
559
- else if (rawPolicy && typeof rawPolicy === "object") {
560
- const keys = Object.keys(rawPolicy);
561
- const hasNumericKeys = keys.some((k) => /^\d+$/.test(k));
562
- if (hasNumericKeys) {
563
- console.warn(
564
- "[tems-modal] obj.policy has numeric keys! Extracting policies array.",
565
- );
566
-
567
- // Check if object has a "target" property
568
- const target = rawPolicy.target;
569
-
570
- // Extract policies from numeric keys
571
- const extractedPolicies = [];
572
- for (const key in rawPolicy) {
573
- if (/^\d+$/.test(key)) {
574
- let policy = rawPolicy[key];
575
- // Add target if it exists and policy doesn't have one
576
- if (target && !policy.target && !policy["odrl:target"]) {
577
- policy = { ...policy, target, "odrl:target": target };
578
- }
579
- extractedPolicies.push(policy);
580
- }
581
- }
582
- if (extractedPolicies.length > 0) {
583
- policies = extractedPolicies;
584
- rawPolicy = extractedPolicies[0]; // Use first as default
585
- }
586
- }
587
- }
182
+ const offer = this.object as DSPOffer;
183
+
184
+ // Extract required fields from props and offer
185
+ const counterPartyAddress =
186
+ this.counterPartyAddress || offer._provider?.address || "";
187
+ const counterPartyId =
188
+ offer._provider?.participantId ||
189
+ this.counterPartyId ||
190
+ this.participantId;
191
+ const datasetId = offer["@id"] || offer.id;
192
+
193
+ // Extract and process policies from new format
194
+ const { policies, defaultPolicy } = dspPolicyHelpers.extractPolicies(
195
+ offer["odrl:hasPolicy"],
196
+ );
588
197
 
589
198
  // Check if there are multiple policies available
590
-
591
199
  if (
592
200
  policies &&
593
201
  policies.length > 1 &&
594
202
  this.selectedPolicyIndex === undefined
595
203
  ) {
596
- // Store policies in state for the modal to access
597
204
  this.availablePolicies = policies;
598
- // Show policy selection UI
599
205
  this.showPolicySelection = true;
600
206
  this.requestUpdate();
601
207
  return;
@@ -607,18 +213,12 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
607
213
  ? this.availablePolicies[this.selectedPolicyIndex]
608
214
  : this.selectedPolicyIndex !== undefined && policies
609
215
  ? policies[this.selectedPolicyIndex]
610
- : rawPolicy;
611
-
612
- this.selectedPolicyIndex !== undefined && this.availablePolicies
613
- ? "availablePolicies[index]"
614
- : this.selectedPolicyIndex !== undefined && policies
615
- ? "policies[index]"
616
- : "rawPolicy (fallback)";
216
+ : defaultPolicy;
617
217
 
618
218
  // Validate required fields
619
219
  if (!counterPartyAddress) {
620
220
  throw new Error(
621
- "No provider endpoint URL (counterPartyAddress) found in service object",
221
+ "No provider endpoint URL (counterPartyAddress) configured",
622
222
  );
623
223
  }
624
224
 
@@ -630,24 +230,16 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
630
230
  throw new Error("No policy found for dataset");
631
231
  }
632
232
 
633
- // FINAL SAFEGUARD: Ensure policy doesn't have numeric keys
634
- if (policy && typeof policy === "object") {
635
- const policyKeys = Object.keys(policy);
636
- const hasNumericKeys = policyKeys.some((k) => /^\d+$/.test(k));
637
- if (hasNumericKeys) {
638
- console.error(
639
- "[tems-modal] ERROR: Policy still has numeric keys after processing!",
640
- policy,
641
- );
642
- throw new Error(
643
- "Invalid policy structure detected. Policy must be a single object, not an array.",
644
- );
645
- }
233
+ // Validate policy structure
234
+ if (!dspPolicyHelpers.validatePolicyStructure(policy)) {
235
+ throw new Error(
236
+ "Invalid policy structure detected. Policy must be a single object, not an array.",
237
+ );
646
238
  }
647
239
 
648
240
  if (!counterPartyId) {
649
241
  throw new Error(
650
- "No participant ID configured. Please provide participant-id attribute or ensure dspace:participantId is in the service data.",
242
+ "No participant ID configured. Please provide participant-id attribute.",
651
243
  );
652
244
  }
653
245
 
@@ -656,15 +248,11 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
656
248
  this.negotiationError = undefined;
657
249
  this.requestUpdate();
658
250
 
659
- // The policy already has the target field set by FederatedCatalogueStore
660
- // and all urn:tems: prefixes have been stripped
661
- const processedPolicy = policy;
662
-
663
251
  // Initiate contract negotiation
664
252
  const negotiationId = await dspStore.negotiateContract(
665
253
  counterPartyAddress,
666
254
  datasetId,
667
- processedPolicy,
255
+ policy,
668
256
  counterPartyId,
669
257
  );
670
258
 
@@ -673,7 +261,7 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
673
261
  this.requestUpdate();
674
262
 
675
263
  // Save initial contract state to catalog
676
- this._saveInitialContractState(negotiationId);
264
+ dspAgreementStorage.saveInitialContractState(offer, negotiationId);
677
265
 
678
266
  // Poll for negotiation status
679
267
  await this._pollNegotiationStatus(dspStore, negotiationId);
@@ -684,7 +272,9 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
684
272
 
685
273
  // Update contract state if negotiation was initiated
686
274
  if (this.negotiationId) {
687
- this._updateContractState(
275
+ const offer = this.object as DSPOffer;
276
+ dspAgreementStorage.updateContractState(
277
+ offer,
688
278
  this.negotiationId,
689
279
  "FAILED",
690
280
  this.negotiationError,
@@ -695,6 +285,9 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
695
285
  }
696
286
  }
697
287
 
288
+ /**
289
+ * Poll for negotiation status
290
+ */
698
291
  async _pollNegotiationStatus(dspStore: any, negotiationId: string) {
699
292
  const maxAttempts = 8;
700
293
  const pollInterval = 5000;
@@ -709,13 +302,13 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
709
302
  this.requestUpdate();
710
303
 
711
304
  if (status.state === "FINALIZED" || status.state === "AGREED") {
712
- // Retrieve contract agreement (pass providerId to properly key the agreement)
713
- const obj = this.object as any;
305
+ const offer = this.object as DSPOffer;
714
306
  const providerId =
715
- obj.counterPartyId ||
716
- obj._providerParticipantId ||
717
- obj._provider ||
307
+ offer._provider?.participantId ||
308
+ this.counterPartyId ||
309
+ this.participantId ||
718
310
  "";
311
+
719
312
  try {
720
313
  const agreement = await dspStore.getContractAgreement(
721
314
  negotiationId,
@@ -729,14 +322,23 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
729
322
  this.contractId = status.contractAgreementId || negotiationId;
730
323
  }
731
324
 
732
- // Save agreement info to localStorage for persistence
325
+ // Save agreement info to localStorage
733
326
  if (this.contractId && negotiationId) {
734
- this._saveAgreementInfo(this.contractId, negotiationId, Date.now());
327
+ dspAgreementStorage.saveAgreementInfo(
328
+ offer,
329
+ this.contractId,
330
+ negotiationId,
331
+ Date.now(),
332
+ );
735
333
  }
736
334
 
737
- // Save contract to DSP Contract Storage for catalog display
335
+ // Save contract to DSP Contract Storage
738
336
  if (this.contractId) {
739
- this._saveToContractCatalog(this.contractId, negotiationId);
337
+ dspAgreementStorage.saveToContractCatalog(
338
+ offer,
339
+ this.contractId,
340
+ negotiationId,
341
+ );
740
342
  }
741
343
 
742
344
  this.negotiationStatus = "granted";
@@ -748,7 +350,9 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
748
350
  this.negotiationStatus = "failed";
749
351
  this.negotiationError =
750
352
  status.errorDetail || "Negotiation terminated";
751
- this._updateContractState(
353
+ const offer = this.object as DSPOffer;
354
+ dspAgreementStorage.updateContractState(
355
+ offer,
752
356
  negotiationId,
753
357
  "TERMINATED",
754
358
  this.negotiationError,
@@ -771,613 +375,29 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
771
375
  this.negotiationStatus = "failed";
772
376
  this.negotiationError =
773
377
  "Negotiation timeout after 40 seconds - may still be processing on provider side";
774
- this._updateContractState(negotiationId, "FAILED", this.negotiationError);
775
- this.requestUpdate();
776
- }
777
-
778
-
779
- _renderBoolean(field: boolean): TemplateResultOrSymbol {
780
- if (field) {
781
- return html`<tems-badge class="badges" type="success" size="sm"
782
- ><icon-ci-check></icon-ci-check
783
- ></tems-badge>`;
784
- }
785
- return html`<tems-badge class="badges" type="error" size="sm"
786
- ><icon-material-symbols-close-rounded></icon-material-symbols-close-rounded
787
- ></tems-badge>`;
788
- }
789
-
790
- _renderDivision(type: string, label: string): TemplateResult {
791
- return html`<tems-division type="${type}"
792
- ><div>${unsafeHTML(String(label))}</div></tems-division
793
- >`;
794
- }
795
-
796
- _renderBadge(type?: string, label?: string): TemplateResultOrSymbol {
797
- if (!label) return nothing;
798
- return html`<tems-badge
799
- type=${type}
800
- label=${label}
801
- size="sm"
802
- ></tems-badge>`;
803
- }
804
-
805
- _renderButton(
806
- iconLeft?: TemplateResult,
807
- size?: string,
808
- label?: string,
809
- type?: string,
810
- url?: string,
811
- iconRight?: TemplateResult,
812
- disabled?: boolean,
813
- ): TemplateResultOrSymbol {
814
- if (!label) return nothing;
815
- return html`<tems-button
816
- .iconLeft=${ifDefined(iconLeft)}
817
- .iconRight=${ifDefined(iconRight)}
818
- size=${ifDefined(size)}
819
- label=${ifDefined(label)}
820
- type=${ifDefined(type)}
821
- url=${ifDefined(url)}
822
- disabled=${disabled || nothing}
823
- ></tems-button>`;
824
- }
825
-
826
- _renderIframe(url: string): TemplateResult {
827
- return html`<iframe src="${url}"></iframe>`;
828
- }
829
-
830
- _renderKindBadgeComponent(
831
- object: rdf.DataOffer | undefined = undefined,
832
- ): TemplateResultOrSymbol {
833
- const data_offer = object || this.object;
834
- if (!data_offer.offers || data_offer.offers.length === 0) return nothing;
835
-
836
- return html`<div class="badges">
837
- ${data_offer.offers.map((offer: rdf.Offer) =>
838
- this._renderBadge("information", offerKindHandler(offer.kind)),
839
- )}
840
- </div> `;
841
- }
842
-
843
- _renderCategoryBadgeComponent(): TemplateResultOrSymbol {
844
- const badgeType: string = this.isType(rdf.RDFTYPE_DATAOFFER)
845
- ? "default"
846
- : "information";
847
- if (!this.object.categories || this.object.categories.length === 0)
848
- return nothing;
849
-
850
- return html`<div class="badges">
851
- ${this.object.categories.length === 0
852
- ? this._renderBadge(badgeType, msg("No category"))
853
- : this.object.categories.map((category: rdf.NamedResource) =>
854
- this._renderBadge(badgeType, category.name || ""),
855
- )}
856
- </div>`;
857
- }
858
-
859
- _renderDescription(): TemplateResult {
860
- return this._renderDivision("body-m", this.object.description);
861
- }
862
-
863
- _renderTitleValueDivision(
864
- title: string,
865
- value: string | undefined,
866
- ): TemplateResultOrSymbol {
867
- if (!value) return nothing;
868
- return html`${this._renderDivision("h4", title)}
869
- ${this._renderDivision("body-m", value)}`;
870
- }
871
-
872
- _renderLicences(): TemplateResult {
873
- return html`<div>
874
- ${this.object.licences.length !== 0
875
- ? html`${this._renderDivision("h4", msg("Licences"))}
876
- ${this.object.licences.map((licence: rdf.Licence) => {
877
- if (!licence.name) return nothing;
878
- return html`<tems-division type="body-m">
879
- ${licence.url
880
- ? html`<a href=${licence.url} target="_blank"
881
- >${licence.name || msg("See more")}
882
- <icon-mingcute-arrow-right-up-line></icon-mingcute-arrow-right-up-line
883
- ></a>`
884
- : html`${licence.name}`}</tems-division
885
- > `;
886
- })}`
887
- : html`${this._renderDivision("h4", msg("Licences"))}
888
- <tems-division type="body-m">-</tems-division>`}
889
- </div>`;
890
- }
891
-
892
- _renderBgImg(imgSrc: string, className: string) {
893
- if (!imgSrc) {
894
- return nothing;
895
- }
896
- return html`<div
897
- class="${className}"
898
- style="background-image: url('${imgSrc}')"
899
- ></div>`;
900
- }
901
-
902
- _renderImageSingle(): TemplateResultOrSymbol {
903
- if (!this.object.image && !this.object.images) {
904
- return nothing;
905
- }
906
-
907
- const images = [];
908
-
909
- if (this.object.image) {
910
- images.push(this.object.image);
911
- }
912
-
913
- if (this.object.images) {
914
- images.push(...this.object.images);
915
- }
916
-
917
- return html`<div class="default-image-grid">
918
- ${images.map((image: rdf.Image) => {
919
- if (image.iframe && image.url) {
920
- return html`${this._renderIframe(image.url)}`;
921
- }
922
-
923
- return html`<img
924
- class="default-img"
925
- src=${image.url}
926
- alt=${ifDefined(image.name)}
927
- ></div>`;
928
- })}
929
- </div>`;
930
- }
931
-
932
- _renderImageArray(): TemplateResultOrSymbol {
933
- const iframe = this.object.images.filter(
934
- (image: rdf.Image) => image.iframe && image.url,
378
+ const offer = this.object as DSPOffer;
379
+ dspAgreementStorage.updateContractState(
380
+ offer,
381
+ negotiationId,
382
+ "FAILED",
383
+ this.negotiationError,
935
384
  );
936
- if (iframe.length > 0) {
937
- return html`${this._renderIframe(iframe[0].url)}`;
938
- }
939
-
940
- const filteredImages = this.object.images.filter(
941
- (image: rdf.Image) => !image.iframe && image.url,
942
- );
943
-
944
- const imgCount = filteredImages.length;
945
-
946
- switch (imgCount) {
947
- case 0:
948
- return nothing;
949
- case 1:
950
- return html`<div
951
- class="main-img"
952
- style="background-image: url(${filteredImages[0].url})"
953
- ></div>`;
954
- case 2:
955
- return html`<div class="main-img case-2">
956
- ${this._renderBgImg(filteredImages[0].url, "full-width")}
957
- ${this._renderBgImg(filteredImages[1].url, "full-width")}
958
- </div>`;
959
- case 3:
960
- return html`<div class="main-img case-3">
961
- ${this._renderBgImg(filteredImages[0].url, "full-width")}
962
- <div class="img-inner-row">
963
- <div class="double-image">
964
- ${this._renderBgImg(filteredImages[1].url, "")}
965
- ${this._renderBgImg(filteredImages[2].url, "")}
966
- </div>
967
- </div>
968
- </div>`;
969
- default:
970
- return html`<div class="main-img case-4">
971
- ${this._renderBgImg(filteredImages[0].url, "full-width")}
972
- <div class="img-inner-row">
973
- <div class="double-image">
974
- ${this._renderBgImg(filteredImages[1].url, "")}
975
- ${this._renderBgImg(filteredImages[2].url, "")}
976
- </div>
977
- ${this._renderBgImg(filteredImages[3].url, "last-img")}
978
- </div>
979
- </div>`;
980
- }
981
- }
982
-
983
- _renderAboutProvider(): TemplateResultOrSymbol {
984
- if (this.object.providers.length === 0) return nothing;
985
-
986
- return html`${this._renderDivision("h4", msg("Providers"))}
987
- ${this.object.providers.map(
988
- (provider: rdf.Provider) =>
989
- html`<div>
990
- <img
991
- src="${provider.image?.url}"
992
- alt=${provider.name}
993
- class="default-img"
994
- />
995
- </div>
996
- ${this._renderTitleValueDivision(
997
- msg("About the providers"),
998
- provider.description || msg("No description provided"),
999
- )}`,
1000
- )}`;
1001
- }
1002
-
1003
- _renderCompatibleServices(): TemplateResultOrSymbol {
1004
- if (this.object.services.length === 0) return nothing;
1005
-
1006
- return html`${this._renderDivision(
1007
- "h4",
1008
- this.isType(rdf.RDFTYPE_PROVIDER)
1009
- ? msg("Available Services")
1010
- : msg("Compatible Services"),
1011
- )}
1012
- ${this.object.services.map(
1013
- (service: rdf.Service) =>
1014
- html`<ds4go-card-dataspace-catalog
1015
- type="vertical"
1016
- header=${ifDefined(service.name)}
1017
- background-img=${ifDefined(service.images?.[0]?.url)}
1018
- full-size=""
1019
- content=${ifDefined(service.description)}
1020
- ></ds4go-card-dataspace-catalog>`,
1021
- )}`;
1022
- }
1023
-
1024
- _renderCompatibleDataOffers(): TemplateResultOrSymbol {
1025
- if (this.object.data_offers.length === 0) return nothing;
1026
-
1027
- return html`${this._renderDivision("h4", msg("Available Data Offers"))}
1028
- ${this.object.data_offers.map(
1029
- (data_offer: rdf.DataOffer) =>
1030
- html`<ds4go-card-dataspace-catalog
1031
- type="vertical"
1032
- header=${ifDefined(data_offer.name)}
1033
- background-img=${ifDefined(data_offer.image?.url)}
1034
- full-size=""
1035
- content=${ifDefined(data_offer.description)}
1036
- .tags=${[{ name: data_offer.name, type: "information" }]}
1037
- ></ds4go-card-dataspace-catalog>`,
1038
- )}`;
1039
- }
1040
- // tags=${this._renderKindBadgeComponent(data_offer)}
1041
-
1042
- _renderOffers(): TemplateResult {
1043
- return html`${this._renderDivision("h4", msg("Offers"))}
1044
- ${this.object.offers.map((offer: rdf.Offer) => {
1045
- const msgSubscribe: string = offerKindActionHandler(offer.kind);
1046
-
1047
- if (!msgSubscribe) return nothing;
1048
- return html`<ds4go-card-dataspace-catalog
1049
- type="vertical"
1050
- header=${ifDefined(offer.name)}
1051
- content=${ifDefined(offer.description)}
1052
- ><div>
1053
- ${this._renderButton(
1054
- undefined,
1055
- "sm",
1056
- msgSubscribe,
1057
- "primary",
1058
- undefined,
1059
- undefined,
1060
- true,
1061
- )}
1062
- </div></ds4go-card-dataspace-catalog
1063
- >`;
1064
- })}`;
1065
- }
1066
-
1067
- _renderColumns(...columns: TemplateResultOrSymbol[]): TemplateResultOrSymbol {
1068
- const filteredColumns = columns.filter((col) => col !== nothing);
1069
-
1070
- if (filteredColumns.length === 1) {
1071
- return columns[0];
1072
- }
1073
-
1074
- return html`<div class="multiple-columns flex flex-row flex-1">
1075
- ${filteredColumns.map(
1076
- (col) => html`<div class="half flex flex-column wrap">${col}</div>`,
1077
- )}
1078
- </div>`;
1079
- }
1080
-
1081
-
1082
- _renderServiceSpecificModal(): TemplateResultOrSymbol {
1083
- return html` ${this._renderColumns(
1084
- html`${this.renderTemplateWhenWith(["release_date"], () =>
1085
- this._renderTitleValueDivision(
1086
- msg("Release Date"),
1087
- formatDate(this.object.release_date),
1088
- ),
1089
- )}
1090
- ${this._renderPolicyDescription()} ${this._renderAgreementInfo()}`,
1091
- )}`;
1092
- }
1093
-
1094
- _renderPolicyDescription(): TemplateResultOrSymbol {
1095
- const obj = this.object as any;
1096
- let policy = obj.policy;
1097
- let policies = obj.policies;
1098
-
1099
- // DEFENSIVE: Handle case where obj.policy might be an array
1100
- if (Array.isArray(policy)) {
1101
- // Extract valid policies from array
1102
- const extractedPolicies = policy.filter(
1103
- (item: any) => item && typeof item === "object" && item["@id"],
1104
- );
1105
- if (extractedPolicies.length > 0) {
1106
- policies = extractedPolicies;
1107
- policy = extractedPolicies[0];
1108
- }
1109
- }
1110
- // DEFENSIVE: Handle case where obj.policy has numeric keys
1111
- else if (policy && typeof policy === "object") {
1112
- const keys = Object.keys(policy);
1113
- const hasNumericKeys = keys.some((k) => /^\d+$/.test(k));
1114
- if (hasNumericKeys) {
1115
- const extractedPolicies = [];
1116
- for (const key in policy) {
1117
- if (/^\d+$/.test(key)) {
1118
- extractedPolicies.push(policy[key]);
1119
- }
1120
- }
1121
- if (extractedPolicies.length > 0) {
1122
- policies = extractedPolicies;
1123
- policy = extractedPolicies[0];
1124
- }
1125
- }
1126
- }
1127
-
1128
- // Check if we have multiple policies
1129
- const hasMultiplePolicies =
1130
- policies && Array.isArray(policies) && policies.length > 1;
1131
-
1132
- // Only show if there's a policy
1133
- if (!policy && (!policies || policies.length === 0)) {
1134
- return nothing;
1135
- }
1136
-
1137
- return html`
1138
- <div
1139
- style="margin-top: 24px; padding: 16px; background: #f0f7ff; border-radius: 8px; border: 1px solid #d0e7ff;"
1140
- >
1141
- ${hasMultiplePolicies
1142
- ? html`
1143
- ${this._renderDivision("h4", msg("Access Policies"))}
1144
- <div
1145
- style="margin-bottom: 12px; color: #0066cc; font-size: 0.9em;"
1146
- >
1147
- ${msg("Multiple contract policies available for this asset")}
1148
- (${policies.length})
1149
- </div>
1150
- ${policies.map(
1151
- (p: any, index: number) => html`
1152
- <div
1153
- style="margin-bottom: 16px; padding: 12px; background: white; border-radius: 6px; border-left: 4px solid #0066cc;"
1154
- >
1155
- <div
1156
- style="font-weight: 600; margin-bottom: 8px; color: #333;"
1157
- >
1158
- ${msg("Policy")} ${index + 1}
1159
- ${p["@id"]
1160
- ? html`
1161
- <span
1162
- style="font-weight: normal; font-size: 0.85em; color: #666; display: block; margin-top: 4px; word-break: break-all; font-family: monospace;"
1163
- >
1164
- ${p["@id"]}
1165
- </span>
1166
- `
1167
- : nothing}
1168
- </div>
1169
- <odrl-policy-viewer .policy=${p}></odrl-policy-viewer>
1170
- </div>
1171
- `,
1172
- )}
1173
- `
1174
- : html`
1175
- ${this._renderDivision("h4", msg("Access Policy"))}
1176
- <odrl-policy-viewer .policy=${policy}></odrl-policy-viewer>
1177
- `}
1178
- </div>
1179
- `;
1180
- }
1181
-
1182
- _renderAgreementInfo(): TemplateResultOrSymbol {
1183
- // Show agreement info after successful negotiation, regardless of API Gateway config
1184
- if (this.negotiationStatus !== "granted" || !this.contractId) {
1185
- return nothing;
1186
- }
1187
-
1188
- const storedInfo = this._loadAgreementInfo();
1189
- const agreementDate = storedInfo?.timestamp
1190
- ? new Date(storedInfo.timestamp).toLocaleString()
1191
- : null;
1192
-
1193
- // Get endpoint URL from asset
1194
- const obj = this.object as any;
1195
- const endpointUrl =
1196
- obj?.endpointUrl ||
1197
- obj?.["dcat:endpointURL"] ||
1198
- obj?.distribution?.endpointUrl;
1199
-
1200
- return html`
1201
- <div
1202
- style="margin-top: 24px; padding: 16px; background: #e8f5e9; border-radius: 8px;"
1203
- >
1204
- ${this._renderDivision("h4", msg("Contract Agreement"))}
1205
-
1206
- <div style="font-size: 0.9em; margin-top: 12px;">
1207
- <div style="margin-bottom: 8px;">
1208
- <strong>✅ ${msg("Agreement ID:")}</strong>
1209
- <div
1210
- style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
1211
- >
1212
- ${this.contractId}
1213
- </div>
1214
- </div>
1215
-
1216
- ${endpointUrl
1217
- ? html`
1218
- <div style="margin-bottom: 8px;">
1219
- <strong>🔗 ${msg("Endpoint URL:")}</strong>
1220
- <div
1221
- style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
1222
- >
1223
- ${endpointUrl}
1224
- </div>
1225
- </div>
1226
- `
1227
- : nothing}
1228
- ${agreementDate
1229
- ? html`
1230
- <div style="opacity: 0.8; font-size: 0.85em;">
1231
- <strong>${msg("Agreed on:")}</strong> ${agreementDate}
1232
- </div>
1233
- `
1234
- : nothing}
1235
- </div>
1236
-
1237
- <div
1238
- style="margin-top: 12px; padding: 12px; background: rgba(0,0,0,0.05); border-radius: 4px; font-size: 0.85em;"
1239
- >
1240
- <div style="margin-bottom: 4px;">
1241
- <strong>ℹ️ ${msg("Note:")}</strong>
1242
- </div>
1243
- <div>
1244
- ${msg(
1245
- "You can now use this agreement ID to access the service through the provider's API or data gateway.",
1246
- )}
1247
- </div>
1248
- </div>
1249
-
1250
- ${storedInfo
1251
- ? html`
1252
- <div
1253
- style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);"
1254
- >
1255
- <button
1256
- @click=${this._renewContract}
1257
- style="font-size: 0.85em; color: #666; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
1258
- >
1259
- 🔄 ${msg("Renegotiate contract")}
1260
- </button>
1261
- </div>
1262
- `
1263
- : nothing}
1264
- </div>
1265
- `;
385
+ this.requestUpdate();
1266
386
  }
1267
387
 
1268
-
1269
- _closeModal() {
388
+ /**
389
+ * Handle close modal
390
+ */
391
+ private _closeModal() {
1270
392
  this.dispatchEvent(new CustomEvent("close"));
1271
393
  }
1272
394
 
1273
- _renderPolicySelection(): TemplateResult {
1274
- const policies = this.availablePolicies || [];
1275
-
1276
- return html`
1277
- <div class="policy-selection-modal">
1278
- <div class="policy-selection-header">
1279
- <h3>${msg("Select a Policy")}</h3>
1280
- <p>
1281
- ${msg(
1282
- "Multiple policies are available for this dataset. Please select one to proceed with the negotiation.",
1283
- )}
1284
- </p>
1285
- </div>
1286
- <div class="policy-selection-list">
1287
- ${policies.map(
1288
- (policy: any, index: number) => html`
1289
- <div
1290
- class="policy-option"
1291
- @click=${() => this._selectPolicy(index)}
1292
- >
1293
- <div class="policy-option-header">
1294
- <strong>${msg("Policy")} ${index + 1}</strong>
1295
- ${policy["@id"]
1296
- ? html`<code>${policy["@id"]}</code>`
1297
- : nothing}
1298
- </div>
1299
- <div class="policy-option-details">
1300
- ${unsafeHTML(this._formatPolicyDetails(policy))}
1301
- </div>
1302
- </div>
1303
- `,
1304
- )}
1305
- </div>
1306
- <div class="policy-selection-actions">
1307
- <tems-button
1308
- type="outline-gray"
1309
- @click=${this._cancelPolicySelection}
1310
- >
1311
- ${msg("Cancel")}
1312
- </tems-button>
1313
- </div>
1314
- </div>
1315
- `;
1316
- }
1317
-
1318
- _renderNegotiationButton(): TemplateResultOrSymbol {
1319
- // Check if DSP connector is configured (now passed as property)
1320
-
1321
- const hasDspConnector =
1322
- this.dspStore !== undefined && this.dspStore !== null;
1323
-
1324
- if (!hasDspConnector) {
1325
- return html`<tems-button disabled=""
1326
- >${msg("Activate this service")}</tems-button
1327
- >`;
1328
- }
1329
-
1330
- // Show policy selection if needed
1331
- if (this.showPolicySelection) {
1332
- return this._renderPolicySelection();
1333
- }
1334
-
1335
- switch (this.negotiationStatus) {
1336
- case "idle":
1337
- return html`<tems-button @click=${this._negotiateAccess}
1338
- >${msg("Negotiate access")}</tems-button
1339
- >`;
1340
-
1341
- case "negotiating":
1342
- return html`<tems-button disabled="">
1343
- ${msg("Negotiating...")}
1344
- </tems-button>`;
1345
-
1346
- case "pending":
1347
- return html`<tems-button disabled="">
1348
- ${this.currentState || msg("Pending")}
1349
- ${this.attempt ? `(${this.attempt}/${this.maxAttempts})` : ""}
1350
- </tems-button>`;
1351
-
1352
- case "granted": {
1353
- return html`
1354
- <tems-button disabled="" type="success">
1355
- ✅ ${msg("Access Granted")}
1356
- </tems-button>
1357
- `;
1358
- }
1359
-
1360
- case "failed":
1361
- return html`
1362
- <div style="display: flex; flex-direction: column; gap: 8px;">
1363
- <tems-button disabled="" type="error">
1364
- ❌ ${msg("Failed")}:
1365
- ${this.negotiationError || msg("Unknown error")}
1366
- </tems-button>
1367
- <tems-button @click=${this._negotiateAccess} type="outline-gray">
1368
- ${msg("Retry")}
1369
- </tems-button>
1370
- </div>
1371
- `;
1372
-
1373
- default:
1374
- return html`<tems-button disabled=""
1375
- >${msg("Activate this service")}</tems-button
1376
- >`;
1377
- }
1378
- }
1379
-
1380
395
  render() {
396
+ const offer = this.object as DSPOffer;
397
+ const { policies, defaultPolicy } = dspPolicyHelpers.extractPolicies(
398
+ offer["odrl:hasPolicy"],
399
+ );
400
+
1381
401
  return html`<div class="modal">
1382
402
  <div class="topbar">
1383
403
  <tems-button
@@ -1385,18 +405,51 @@ export class Ds4goCatalogModal extends TemsObjectHandler {
1385
405
  type="outline-gray"
1386
406
  .iconLeft=${html`<icon-material-symbols-close-rounded></icon-material-symbols-close-rounded>`}
1387
407
  ></tems-button>
1388
- <tems-button disabled="">${msg("Integrate Externally")}</tems-button>
1389
- ${this._renderNegotiationButton()}
408
+ ${this.showPolicySelection
409
+ ? nothing
410
+ : html`<catalog-modal-negotiation-button
411
+ .dspStore=${this.dspStore}
412
+ .showPolicySelection=${this.showPolicySelection}
413
+ .negotiationStatus=${this.negotiationStatus}
414
+ .currentState=${this.currentState}
415
+ .attempt=${this.attempt}
416
+ .maxAttempts=${this.maxAttempts}
417
+ .negotiationError=${this.negotiationError}
418
+ @negotiate=${this._negotiateAccess}
419
+ @retry=${this._negotiateAccess}
420
+ ></catalog-modal-negotiation-button>`}
1390
421
  </div>
1391
422
  <div class="modal-content-wrapper">
1392
423
  <div class="modal-box">
1393
424
  <div class="modal-content">
1394
- ${this._renderDivision("h2", this.object.name)}
1395
- ${this.renderTemplateWhenWith(
1396
- ["description"],
1397
- this._renderDescription,
1398
- )}
1399
- ${this._renderServiceSpecificModal()}
425
+ ${this.showPolicySelection
426
+ ? html`<catalog-modal-policy-selection
427
+ .policies=${policies}
428
+ @policy-selected=${this._handlePolicySelected}
429
+ @policy-cancel=${this._handlePolicyCancel}
430
+ ></catalog-modal-policy-selection>`
431
+ : html`<tems-division type="h2"
432
+ ><div>${offer.name || ""}</div></tems-division
433
+ >
434
+ ${offer.description
435
+ ? html`<tems-division type="body-m"
436
+ ><div>${offer.description}</div></tems-division
437
+ >`
438
+ : nothing}
439
+ <div class="multiple-columns flex flex-row flex-1">
440
+ <catalog-modal-policy-display
441
+ .policy=${defaultPolicy}
442
+ .policies=${policies}
443
+ ></catalog-modal-policy-display>
444
+ <catalog-modal-agreement-info
445
+ .offer=${offer}
446
+ .agreementInfo=${dspAgreementStorage.loadAgreementInfo(
447
+ offer,
448
+ )}
449
+ .contractId=${this.contractId}
450
+ @renew-contract=${this._renewContract}
451
+ ></catalog-modal-agreement-info>
452
+ </div>`}
1400
453
  </div>
1401
454
  </div>
1402
455
  </div>