@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
@@ -0,0 +1,128 @@
1
+ import Ds4goCardFactStyle from "@styles/cards/ds4go-card-fact.scss?inline";
2
+ import { formatDate } from "@helpers";
3
+ import type { Category, Fact } from "@src/ds4go";
4
+ import { css, html, LitElement, nothing, unsafeCSS } from "lit";
5
+ import { customElement, property } from "lit/decorators.js";
6
+ import { msg, str } from "@lit/localize";
7
+
8
+ @customElement("ds4go-card-fact")
9
+ export class Ds4goCardFact extends LitElement {
10
+ static styles = css`
11
+ ${unsafeCSS(Ds4goCardFactStyle)};
12
+ `;
13
+
14
+ @property({ attribute: false })
15
+ object?: Fact;
16
+
17
+ private _getThumbnail(): string {
18
+ if (this.object?.enclosure) {
19
+ return String(this.object.enclosure);
20
+ }
21
+ // Default placeholder image
22
+ return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='354' height='167' viewBox='0 0 354 167'%3E%3Crect width='354' height='167' fill='%23f0f0f0'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%23999'%3ENo Image%3C/text%3E%3C/svg%3E";
23
+ }
24
+
25
+ private _getFormattedDate(): string {
26
+ if (!this.object?.created_at) return "";
27
+ return formatDate(this.object.created_at, {
28
+ year: "numeric",
29
+ month: "long",
30
+ day: "numeric",
31
+ });
32
+ }
33
+
34
+ private _getTags() {
35
+ const tags: {
36
+ label: string;
37
+ type: string;
38
+ color?: string;
39
+ }[] = [];
40
+
41
+ // Add categories as tags (max 3, with overflow indicator)
42
+ (this.object?.categories || [])?.slice(0, 3).forEach((c: Category) => {
43
+ if (c.name) {
44
+ tags.push({
45
+ label: c.name,
46
+ type: "neutral",
47
+ });
48
+ }
49
+ });
50
+
51
+ const totalCategories = this.object?.categories?.length || 0;
52
+ if (totalCategories > 3) {
53
+ tags.push({
54
+ label: msg(str`+${totalCategories - 3} ${msg("more")}`),
55
+ type: "neutral",
56
+ });
57
+ }
58
+
59
+ // Add media count if available
60
+ const mediaCount = this.object?.medias?.length || 0;
61
+ if (mediaCount > 0) {
62
+ tags.push({
63
+ label: msg(
64
+ str`${mediaCount} ${mediaCount > 1 ? msg("medias") : msg("media")}`,
65
+ ),
66
+ type: "information",
67
+ });
68
+ }
69
+
70
+ return tags;
71
+ }
72
+
73
+ render() {
74
+ if (!this.object) return nothing;
75
+ const tags = this._getTags();
76
+ const formattedDate = this._getFormattedDate();
77
+
78
+ return html`<article class="vertical">
79
+ <header
80
+ class="image"
81
+ style='background-image: url("${this._getThumbnail()}")'
82
+ ></header>
83
+ <main>
84
+ ${formattedDate
85
+ ? html`<div class="meta-info">
86
+ <span class="date">${formattedDate}</span>
87
+ </div>`
88
+ : nothing}
89
+ <div class="content">
90
+ ${this.object?.name
91
+ ? html`<div class="card-header">
92
+ <tems-division
93
+ label=${this.object?.name}
94
+ type="h4"
95
+ ></tems-division>
96
+ </div>`
97
+ : nothing}
98
+ ${this.object?.description
99
+ ? html`<div class="card-content">
100
+ <tems-division
101
+ label=${this.object?.description}
102
+ type="body-m"
103
+ ></tems-division>
104
+ </div>`
105
+ : nothing}
106
+ ${tags.length > 0
107
+ ? html`<div class="tags">
108
+ ${tags.map(
109
+ (tag) =>
110
+ html`<tems-badge
111
+ size="sm"
112
+ label=${tag.label}
113
+ type=${tag.type}
114
+ color=${tag.color || nothing}
115
+ ></tems-badge>`,
116
+ )}
117
+ </div>`
118
+ : nothing}
119
+ ${this.object.author
120
+ ? html`<div class="meta-info">
121
+ <span class="author">${msg("By")} ${this.object.author}</span>
122
+ </div>`
123
+ : nothing}
124
+ </div>
125
+ </main>
126
+ </article>`;
127
+ }
128
+ }
@@ -0,0 +1,158 @@
1
+ import type { DSPOffer } from "@src/ds4go";
2
+ import {
3
+ filterObjectByDateAfter,
4
+ filterObjectById,
5
+ filterObjectByInterval,
6
+ filterObjectByNamedValue,
7
+ filterObjectByType,
8
+ filterObjectByValue,
9
+ } from "@helpers";
10
+ import type { Resource } from "@src/component";
11
+ import {
12
+ TemsObjectsHandler,
13
+ type TemsSearchObject,
14
+ } from "@startinblox/solid-tems-shared";
15
+ import { css, html, nothing, type PropertyValues } from "lit";
16
+ import { customElement, state } from "lit/decorators.js";
17
+
18
+ @customElement("ds4go-catalog-data-holder")
19
+ export class Ds4goCatalogDataHolder extends TemsObjectsHandler {
20
+ static styles = css`
21
+ .card-grid {
22
+ display: flex;
23
+ flex-direction: row;
24
+ flex-wrap: wrap;
25
+ gap: 20px;
26
+ }
27
+ .card-grid-vertical {
28
+ justify-content: stretch;
29
+ }
30
+ .card-grid-vertical ds4go-card-dataspace-catalog {
31
+ width: 354px;
32
+ height: auto;
33
+ }
34
+ ds4go-card-dataspace-catalog {
35
+ cursor: pointer;
36
+ }
37
+ `;
38
+
39
+ @state()
40
+ view: "card" | "list" | "table" | "map" = "card";
41
+
42
+ @state()
43
+ search: TemsSearchObject[] = [];
44
+
45
+ objects: Resource[] = [];
46
+
47
+ @state()
48
+ protected _displayObjects: DSPOffer[] = [];
49
+
50
+ protected filter(objects: any[], filters: TemsSearchObject[] = []) {
51
+ if (!filters || filters.length === 0 || !objects || objects.length === 0) {
52
+ return objects;
53
+ }
54
+
55
+ const groupedFilters = new Map<string, TemsSearchObject[]>();
56
+ for (const filter of filters) {
57
+ const groupKey = filter.name;
58
+ const group = groupedFilters.get(groupKey) || [];
59
+ group.push(filter);
60
+ groupedFilters.set(groupKey, group);
61
+ }
62
+
63
+ let currentFilteredObjects = [...objects];
64
+
65
+ for (const filterGroup of groupedFilters.values()) {
66
+ if (currentFilteredObjects.length === 0) {
67
+ break;
68
+ }
69
+
70
+ const tempResults = [];
71
+ for (const filter of filterGroup) {
72
+ switch (filter.type) {
73
+ case "interval":
74
+ tempResults.push(
75
+ filterObjectByInterval(
76
+ currentFilteredObjects,
77
+ filter.name,
78
+ filter.value,
79
+ ),
80
+ );
81
+ break;
82
+ case "dateAfter":
83
+ tempResults.push(
84
+ filterObjectByDateAfter(
85
+ currentFilteredObjects,
86
+ filter.name,
87
+ filter.value,
88
+ ),
89
+ );
90
+ break;
91
+ case "matchId":
92
+ tempResults.push(
93
+ filterObjectById(
94
+ currentFilteredObjects,
95
+ filter.name,
96
+ filter.value,
97
+ ),
98
+ );
99
+ break;
100
+ case "matchType":
101
+ tempResults.push(
102
+ filterObjectByType(currentFilteredObjects, filter.value),
103
+ );
104
+ break;
105
+ case "exact":
106
+ tempResults.push(
107
+ filterObjectByNamedValue(
108
+ currentFilteredObjects,
109
+ filter.name,
110
+ filter.value,
111
+ ),
112
+ );
113
+ break;
114
+ default:
115
+ tempResults.push(
116
+ filterObjectByValue(currentFilteredObjects, filter.value),
117
+ );
118
+ }
119
+ }
120
+ currentFilteredObjects = [...tempResults.flat()];
121
+ }
122
+
123
+ return currentFilteredObjects;
124
+ }
125
+
126
+ protected _handleClickEvent(originalObj: Resource) {
127
+ this.dispatchEvent(new CustomEvent("clicked", { detail: originalObj }));
128
+ }
129
+
130
+ willUpdate(changedProperties: PropertyValues) {
131
+ if (changedProperties.has("objects") || changedProperties.has("search")) {
132
+ const filteredObjects = this.filter(this.objects, this.search);
133
+ this.dispatchEvent(
134
+ new CustomEvent("result-count", { detail: filteredObjects.length }),
135
+ );
136
+
137
+ this._displayObjects = filteredObjects as DSPOffer[];
138
+ }
139
+ }
140
+
141
+ render() {
142
+ if (!this._displayObjects || this._displayObjects.length === 0) {
143
+ return nothing;
144
+ }
145
+
146
+ return html`<div
147
+ class="card-grid${this.view === "card" ? " card-grid-vertical" : ""}"
148
+ >
149
+ ${this._displayObjects.map((dspOffer) => {
150
+ return html`<ds4go-card-dataspace-catalog
151
+ .object=${dspOffer}
152
+ type=${this.view === "card" ? "card" : "horizontal"}
153
+ @click=${() => this._handleClickEvent(dspOffer as Resource)}
154
+ ></ds4go-card-dataspace-catalog>`;
155
+ })}
156
+ </div>`;
157
+ }
158
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ ComponentObjectsHandler,
3
+ filterObjectByDateAfter,
4
+ filterObjectById,
5
+ filterObjectByInterval,
6
+ filterObjectByNamedValue,
7
+ filterObjectByType,
8
+ filterObjectByValue,
9
+ } from "@helpers";
10
+ import type { Fact } from "@src/ds4go";
11
+ import type { Resource, SearchObject, UnknownResource } from "@src/component";
12
+ import { css, html, nothing, type PropertyValues } from "lit";
13
+ import { customElement, state } from "lit/decorators.js";
14
+
15
+ @customElement("ds4go-fact-holder")
16
+ export class Ds4goFactHolder extends ComponentObjectsHandler {
17
+ static styles = css`
18
+ .card-grid {
19
+ display: flex;
20
+ flex-direction: row;
21
+ flex-wrap: wrap;
22
+ gap: 20px;
23
+ }
24
+ .card-grid-vertical {
25
+ justify-content: stretch;
26
+ }
27
+ .card-grid-vertical ds4go-card-fact {
28
+ width: 354px;
29
+ height: auto;
30
+ }
31
+ `;
32
+
33
+ @state()
34
+ search: SearchObject[] = [];
35
+
36
+ @state()
37
+ protected _displayObjects: Resource[] = [];
38
+
39
+ protected filter(objects: Resource[], filters: SearchObject[] = []) {
40
+ if (!filters || filters.length === 0 || !objects || objects.length === 0) {
41
+ return objects;
42
+ }
43
+
44
+ const groupedFilters = new Map<string, SearchObject[]>();
45
+ for (const filter of filters) {
46
+ const groupKey = filter.name;
47
+ const group = groupedFilters.get(groupKey) || [];
48
+ group.push(filter);
49
+ groupedFilters.set(groupKey, group);
50
+ }
51
+
52
+ let currentFilteredObjects = [...objects];
53
+
54
+ for (const filterGroup of groupedFilters.values()) {
55
+ if (currentFilteredObjects.length === 0) {
56
+ break;
57
+ }
58
+
59
+ const tempResults: UnknownResource[] = [];
60
+ for (const filter of filterGroup) {
61
+ switch (filter.type) {
62
+ case "interval":
63
+ tempResults.push(
64
+ filterObjectByInterval(
65
+ currentFilteredObjects,
66
+ filter.name,
67
+ filter.value,
68
+ ),
69
+ );
70
+ break;
71
+ case "dateAfter":
72
+ tempResults.push(
73
+ filterObjectByDateAfter(
74
+ currentFilteredObjects,
75
+ filter.name,
76
+ filter.value,
77
+ ),
78
+ );
79
+ break;
80
+ case "matchId":
81
+ tempResults.push(
82
+ filterObjectById(
83
+ currentFilteredObjects,
84
+ filter.name,
85
+ filter.value,
86
+ ),
87
+ );
88
+ break;
89
+ case "matchType":
90
+ tempResults.push(
91
+ filterObjectByType(currentFilteredObjects, filter.value),
92
+ );
93
+ break;
94
+ case "exact":
95
+ tempResults.push(
96
+ filterObjectByNamedValue(
97
+ currentFilteredObjects,
98
+ filter.name,
99
+ filter.value,
100
+ ),
101
+ );
102
+ break;
103
+ default:
104
+ tempResults.push(
105
+ filterObjectByValue(currentFilteredObjects, filter.value),
106
+ );
107
+ }
108
+ }
109
+ currentFilteredObjects = [...tempResults.flat()] as Resource[];
110
+ }
111
+
112
+ return currentFilteredObjects;
113
+ }
114
+
115
+ protected _handleClickEvent(originalObj: Resource) {
116
+ this.dispatchEvent(new CustomEvent("clicked", { detail: originalObj }));
117
+ }
118
+
119
+ willUpdate(changedProperties: PropertyValues) {
120
+ if (changedProperties.has("objects") || changedProperties.has("search")) {
121
+ if (this.objects) {
122
+ this._displayObjects = this.filter(this.objects, this.search);
123
+ } else {
124
+ this._displayObjects = [];
125
+ }
126
+ this.dispatchEvent(
127
+ new CustomEvent("result-count", {
128
+ detail: this._displayObjects.length,
129
+ }),
130
+ );
131
+ }
132
+ }
133
+
134
+ render() {
135
+ if (!this._displayObjects || this._displayObjects.length === 0) {
136
+ return nothing;
137
+ }
138
+
139
+ return html`<div class="card-grid card-grid-vertical">
140
+ ${this._displayObjects.map((displayObj) => {
141
+ const fact = displayObj as Fact;
142
+ return html`<ds4go-card-fact
143
+ .object=${import.meta.env.DEV ? fact : nothing}
144
+ @click=${() => this._handleClickEvent(fact)}
145
+ ></ds4go-card-fact>`;
146
+ })}
147
+ </div>`;
148
+ }
149
+ }
@@ -0,0 +1,110 @@
1
+ import { localized, msg } from "@lit/localize";
2
+ import { html, nothing, type TemplateResult } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+ import { LitElement } from "lit";
5
+ import type { DSPOffer, AgreementInfo } from "@src/ds4go";
6
+
7
+ /**
8
+ * Agreement info component for displaying contract agreement details
9
+ */
10
+ @customElement("catalog-modal-agreement-info")
11
+ @localized()
12
+ export class CatalogModalAgreementInfo extends LitElement {
13
+ @property({ attribute: false })
14
+ offer?: DSPOffer;
15
+
16
+ @property({ attribute: false })
17
+ agreementInfo?: AgreementInfo;
18
+
19
+ @property({ attribute: false })
20
+ contractId?: string;
21
+
22
+ private _handleRenewContract(): void {
23
+ this.dispatchEvent(
24
+ new CustomEvent("renew-contract", {
25
+ bubbles: true,
26
+ composed: true,
27
+ }),
28
+ );
29
+ }
30
+
31
+ render(): TemplateResult | typeof nothing {
32
+ // Only show if we have agreement info
33
+ if (!this.contractId || !this.agreementInfo) {
34
+ return nothing;
35
+ }
36
+
37
+ const agreementDate = this.agreementInfo.timestamp
38
+ ? new Date(this.agreementInfo.timestamp).toLocaleString()
39
+ : null;
40
+
41
+ // Get endpoint URL from asset
42
+ const endpointUrl = this.offer?.["dcat:endpointUrl"];
43
+
44
+ return html`<div
45
+ style="margin-top: 24px; padding: 16px; background: #e8f5e9; border-radius: 8px;"
46
+ >
47
+ <tems-division type="h4"
48
+ ><div>${msg("Contract Agreement")}</div></tems-division
49
+ >
50
+ <div style="font-size: 0.9em; margin-top: 12px;">
51
+ <div style="margin-bottom: 8px;">
52
+ <strong>✅ ${msg("Agreement ID:")}</strong>
53
+ <div
54
+ style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
55
+ >
56
+ ${this.contractId}
57
+ </div>
58
+ </div>
59
+
60
+ ${endpointUrl
61
+ ? html`
62
+ <div style="margin-bottom: 8px;">
63
+ <strong>🔗 ${msg("Endpoint URL:")}</strong>
64
+ <div
65
+ style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
66
+ >
67
+ ${endpointUrl}
68
+ </div>
69
+ </div>
70
+ `
71
+ : nothing}
72
+ ${agreementDate
73
+ ? html`
74
+ <div style="opacity: 0.8; font-size: 0.85em;">
75
+ <strong>${msg("Agreed on:")}</strong> ${agreementDate}
76
+ </div>
77
+ `
78
+ : nothing}
79
+ </div>
80
+
81
+ <div
82
+ style="margin-top: 12px; padding: 12px; background: rgba(0,0,0,0.05); border-radius: 4px; font-size: 0.85em;"
83
+ >
84
+ <div style="margin-bottom: 4px;">
85
+ <strong>ℹ️ ${msg("Note:")}</strong>
86
+ </div>
87
+ <div>
88
+ ${msg(
89
+ "You can now use this agreement ID to access the service through the provider's API or data gateway.",
90
+ )}
91
+ </div>
92
+ </div>
93
+
94
+ ${this.agreementInfo
95
+ ? html`
96
+ <div
97
+ style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);"
98
+ >
99
+ <button
100
+ @click=${this._handleRenewContract}
101
+ style="font-size: 0.85em; color: #666; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
102
+ >
103
+ 🔄 ${msg("Renegotiate contract")}
104
+ </button>
105
+ </div>
106
+ `
107
+ : nothing}
108
+ </div>`;
109
+ }
110
+ }
@@ -0,0 +1,4 @@
1
+ export { CatalogModalPolicySelection } from "./policy-selection";
2
+ export { CatalogModalPolicyDisplay } from "./policy-display";
3
+ export { CatalogModalAgreementInfo } from "./agreement-info";
4
+ export { CatalogModalNegotiationButton } from "./negotiation-button";
@@ -0,0 +1,111 @@
1
+ import { localized, msg } from "@lit/localize";
2
+ import { html, type TemplateResult } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+ import { LitElement } from "lit";
5
+
6
+ /**
7
+ * Negotiation button component for handling DSP contract negotiation UI
8
+ */
9
+ @customElement("catalog-modal-negotiation-button")
10
+ @localized()
11
+ export class CatalogModalNegotiationButton extends LitElement {
12
+ @property({ attribute: false })
13
+ dspStore?: any;
14
+
15
+ @property({ attribute: false })
16
+ showPolicySelection = false;
17
+
18
+ @property()
19
+ negotiationStatus:
20
+ | "idle"
21
+ | "negotiating"
22
+ | "pending"
23
+ | "granted"
24
+ | "failed"
25
+ | "transferring" = "idle";
26
+
27
+ @property()
28
+ currentState?: string;
29
+
30
+ @property()
31
+ attempt?: number;
32
+
33
+ @property()
34
+ maxAttempts?: number;
35
+
36
+ @property()
37
+ negotiationError?: string;
38
+
39
+ private _handleNegotiate(): void {
40
+ this.dispatchEvent(
41
+ new CustomEvent("negotiate", {
42
+ bubbles: true,
43
+ composed: true,
44
+ }),
45
+ );
46
+ }
47
+
48
+ private _handleRetry(): void {
49
+ this.dispatchEvent(
50
+ new CustomEvent("retry", {
51
+ bubbles: true,
52
+ composed: true,
53
+ }),
54
+ );
55
+ }
56
+
57
+ render(): TemplateResult {
58
+ const hasDspConnector =
59
+ this.dspStore !== undefined && this.dspStore !== null;
60
+
61
+ if (!hasDspConnector) {
62
+ return html`<tems-button disabled=""
63
+ >${msg("Activate this service")}</tems-button
64
+ >`;
65
+ }
66
+
67
+ switch (this.negotiationStatus) {
68
+ case "idle":
69
+ return html`<tems-button @click=${this._handleNegotiate}
70
+ >${msg("Negotiate access")}</tems-button
71
+ >`;
72
+
73
+ case "negotiating":
74
+ return html`<tems-button disabled="">
75
+ ${msg("Negotiating...")}
76
+ </tems-button>`;
77
+
78
+ case "pending":
79
+ return html`<tems-button disabled="">
80
+ ${this.currentState || msg("Pending")}
81
+ ${this.attempt ? `(${this.attempt}/${this.maxAttempts})` : ""}
82
+ </tems-button>`;
83
+
84
+ case "granted": {
85
+ return html`
86
+ <tems-button disabled="" type="success">
87
+ ✅ ${msg("Access Granted")}
88
+ </tems-button>
89
+ `;
90
+ }
91
+
92
+ case "failed":
93
+ return html`<div
94
+ style="display: flex; flex-direction: column; gap: 8px;"
95
+ >
96
+ <tems-button disabled="" type="error">
97
+ ❌ ${msg("Failed")}:
98
+ ${this.negotiationError || msg("Unknown error")}
99
+ </tems-button>
100
+ <tems-button @click=${this._handleRetry} type="outline-gray">
101
+ ${msg("Retry")}
102
+ </tems-button>
103
+ </div>`;
104
+
105
+ default:
106
+ return html`<tems-button disabled=""
107
+ >${msg("Activate this service")}</tems-button
108
+ >`;
109
+ }
110
+ }
111
+ }