@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,217 @@
1
+ import { formatDate, formatCase, OrbitComponent } from "@helpers";
2
+ import { localized, msg } from "@lit/localize";
3
+ import { Task } from "@lit/task";
4
+ import { PropertiesPicker } from "@src/component";
5
+ import type { Category, Fact } from "@src/ds4go";
6
+
7
+ import ModalStyle from "@styles/modal/ds4go-fact-modal.scss?inline";
8
+ import { css, html, nothing, unsafeCSS } from "lit";
9
+ import { customElement, state } from "lit/decorators.js";
10
+
11
+ @customElement("ds4go-fact-modal")
12
+ @localized()
13
+ export class Ds4goFactModal extends OrbitComponent {
14
+ constructor() {
15
+ super({ setupSubscriptions: false, ignoreRouter: true });
16
+ }
17
+
18
+ static styles = css`
19
+ ${unsafeCSS(ModalStyle)}
20
+ `;
21
+
22
+ cherryPickedProperties: PropertiesPicker[] = [
23
+ { key: "created_at", value: "created_at" },
24
+ { key: "updated_at", value: "updated_at", cast: formatDate },
25
+ { key: "name", value: "name" },
26
+ { key: "description", value: "description" },
27
+ { key: "author", value: "author" },
28
+ { key: "link", value: "link" },
29
+ { key: "enclosure", value: "enclosure" },
30
+ { key: "categories", value: "categories", expand: true },
31
+ { key: "medias", value: "medias", expand: true },
32
+ { key: "url", value: "url" },
33
+ // { key: "file_size", value: "file_size" },
34
+ // { key: "file_type", value: "file_type" },
35
+ // { key: "width", value: "width" },
36
+ // { key: "height", value: "height" },
37
+ // Can't get raw review json, as store does not support datas without ids
38
+ // { key: "review", value: "review" },
39
+ ];
40
+
41
+ @state()
42
+ disableButton = false;
43
+
44
+ _generateVideo() {
45
+ this.orbit?.Swal.fire({
46
+ title: msg("Generate with Tralalère"),
47
+ text: msg(
48
+ "This will create an AI-Generated video of this fact.\nYou will be charged 5€ on your account.",
49
+ ),
50
+ icon: "warning",
51
+ position: "center",
52
+ backdrop: true,
53
+ showCancelButton: true,
54
+ confirmButtonText: msg("Yes, generate it!"),
55
+ cancelButtonText: msg("Cancel"),
56
+ customClass: {
57
+ confirmButton:
58
+ "button text-xsmall text-bold text-center reversed color-secondary bordered icon icon-check icon-margin-right-xsmall no-background-image",
59
+ cancelButton:
60
+ "button text-xsmall text-bold text-center reversed color-primary bordered icon icon-exclamation icon-margin-right-xsmall no-background-image",
61
+ },
62
+ }).then((result: any) => {
63
+ if (result.isConfirmed) {
64
+ this.orbit?.Swal.fire({
65
+ title: msg("Generating..."),
66
+ text: msg("Please wait while Tralalère generates your AI-Video..."),
67
+ icon: "info",
68
+ showConfirmButton: false,
69
+ allowOutsideClick: false,
70
+ allowEscapeKey: false,
71
+ allowEnterKey: false,
72
+ });
73
+ setTimeout(() => {
74
+ this.orbit?.Swal.fire({
75
+ title: msg("AI-Video generated!"),
76
+ text: msg(
77
+ "Your AI-Generated video has been generated and is available in your Tralalère space.",
78
+ ),
79
+ icon: "success",
80
+ });
81
+ this.disableButton = true;
82
+ }, 2500);
83
+ }
84
+ });
85
+ }
86
+
87
+ _closeModal() {
88
+ this.dispatchEvent(new CustomEvent("close"));
89
+ }
90
+
91
+ _getResource = new Task(this, {
92
+ task: async () => {
93
+ if (!this.object) return;
94
+
95
+ this.object = await this._getProxyValue(this.object["@id"]);
96
+
97
+ return this.object;
98
+ },
99
+ args: () => [this.caching, this.currentRoute],
100
+ });
101
+
102
+ render() {
103
+ return this._getResource.render({
104
+ pending: () => html`<solid-loader></solid-loader>`,
105
+ complete: (obj?: Fact) => {
106
+ if (!obj) {
107
+ this._closeModal();
108
+ return nothing;
109
+ }
110
+
111
+ const formattedDate = formatDate(obj.created_at, {
112
+ year: "numeric",
113
+ month: "long",
114
+ day: "numeric",
115
+ hour: "2-digit",
116
+ minute: "2-digit",
117
+ hour12: false,
118
+ });
119
+
120
+ return html`<div class="modal">
121
+ <div class="topbar">
122
+ <tems-button
123
+ @click=${this._closeModal}
124
+ type="outline-gray"
125
+ .iconLeft=${html`<icon-material-symbols-close-rounded></icon-material-symbols-close-rounded>`}
126
+ ></tems-button>
127
+ <tems-button
128
+ @click=${this._generateVideo}
129
+ disabled=${this.disableButton || nothing}
130
+ >${this.disableButton
131
+ ? msg("Video already generated")
132
+ : msg("Generate an AI-Video with Tralalère (5€)")}</tems-button
133
+ >
134
+ </div>
135
+ <div class="modal-content-wrapper">
136
+ <div class="modal-box">
137
+ <div class="modal-content">
138
+ ${obj.enclosure
139
+ ? html`<div class="cover-image">
140
+ <img src=${String(obj.enclosure)} alt="Cover image" />
141
+ </div>`
142
+ : nothing}
143
+ ${obj.author
144
+ ? html`<tems-division type="body-sm" class="author-section">
145
+ <div>
146
+ ${msg("Published on")} ${formattedDate} | ${msg("By")}
147
+ ${String(obj.author)}
148
+ </div>
149
+ </tems-division>`
150
+ : nothing}
151
+ <tems-division type="h3"
152
+ ><div>${String(obj.name || "")}</div></tems-division
153
+ >
154
+ <tems-division type="body-m"
155
+ ><div>${String(obj.description || "")}</div></tems-division
156
+ >
157
+ ${obj.link
158
+ ? html`<tems-division type="body-sm">
159
+ <tems-button
160
+ type="outline-color"
161
+ .label=${msg("Open link on a new tab")}
162
+ @click=${() => window.open(String(obj.link), "_blank")}
163
+ ></tems-button>
164
+ </tems-division>`
165
+ : nothing}
166
+ ${(obj.medias || []).length > 0
167
+ ? html`<tems-division type="h4"
168
+ >${msg("Media files")}</tems-division
169
+ >
170
+ <div class="medias-gallery">
171
+ ${(obj.medias || []).map(
172
+ (media: any) =>
173
+ html`<div class="media-item">
174
+ ${media.url
175
+ ? html`<img
176
+ src=${String(media.url)}
177
+ alt=${media.description || "Media"}
178
+ @click=${() =>
179
+ window.open(String(media.url), "_blank")}
180
+ />`
181
+ : nothing}
182
+ </div>`,
183
+ )}
184
+ </div>`
185
+ : nothing}
186
+ ${(obj.categories || []).length > 0
187
+ ? html`<tems-division type="h4"
188
+ >${msg("Categories")}</tems-division
189
+ >
190
+ <div class="categories-list">
191
+ ${(obj.categories || []).map(
192
+ (c: Category) =>
193
+ html`<tems-badge type="neutral" size="sm"
194
+ >${formatCase(String(c.name))}</tems-badge
195
+ >`,
196
+ )}
197
+ </div>`
198
+ : nothing}
199
+ <tems-division type="body-sm"
200
+ ><div>
201
+ <div>${msg("Updated on")} ${String(obj.updated_at)}</div>
202
+ </div></tems-division
203
+ >
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>`;
208
+ },
209
+ error: (e) => {
210
+ if (import.meta.env.DEV) {
211
+ console.error(e, this);
212
+ }
213
+ return nothing;
214
+ },
215
+ });
216
+ }
217
+ }
@@ -930,7 +930,7 @@ export class PolicyComposer extends LitElement {
930
930
  </fieldset>
931
931
  <div class="flex align-right spacer-top">
932
932
  <tems-button type="outline-gray" size="sm" @click=${this._resetPolicy}>
933
- ${msg(`Reset to ${this.template.name} default`)}
933
+ ${msg(str`Reset to ${this.template.name} default`)}
934
934
  </tems-button>
935
935
  </div>
936
936
  </form>`;
@@ -1,18 +1,3 @@
1
- /**
2
- * ODRL Policy Viewer Component
3
- *
4
- * A Lit web component for displaying ODRL policies in a user-friendly format.
5
- *
6
- * Usage:
7
- * ```html
8
- * <odrl-policy-viewer .policy=${policyObject}></odrl-policy-viewer>
9
- * ```
10
- *
11
- * Attributes:
12
- * - policy: OdrlPolicy object to display
13
- * - compact: boolean (optional) - Show compact view
14
- */
15
-
16
1
  import {
17
2
  type OdrlPolicy,
18
3
  OdrlPolicyRenderer,
@@ -171,9 +156,3 @@ export class OdrlPolicyViewer extends LitElement {
171
156
  }
172
157
  }
173
158
  }
174
-
175
- declare global {
176
- interface HTMLElementTagNameMap {
177
- "odrl-policy-viewer": OdrlPolicyViewer;
178
- }
179
- }
@@ -1,5 +1,4 @@
1
1
  import * as utils from "@helpers";
2
- import { msg, str } from "@lit/localize";
3
2
  import { Task } from "@lit/task";
4
3
  import type {
5
4
  Resource,
@@ -12,13 +11,6 @@ import {
12
11
  import { css, html, nothing } from "lit";
13
12
  import { customElement, property, state } from "lit/decorators.js";
14
13
 
15
- export interface DSPProviderConfig {
16
- name: string;
17
- address: string;
18
- color?: string;
19
- participantId?: string;
20
- }
21
-
22
14
  @customElement("solid-dsp-catalog")
23
15
  export class DSPCatalog extends OrbitDSPComponent {
24
16
  constructor() {
@@ -175,40 +167,7 @@ export class DSPCatalog extends OrbitDSPComponent {
175
167
  return html`<tems-viewport>
176
168
  <tems-header slot="header" heading=${this.header}></tems-header>
177
169
  <div slot="content">
178
- ${this.providers.length > 0
179
- ? html`<div
180
- style="background: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;"
181
- >
182
- <h3 style="margin: 0 0 1rem 0;">
183
- ${msg("Provider Statistics")}
184
- </h3>
185
- <div
186
- style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;"
187
- >
188
- ${this.providers.map((provider: DSPProviderConfig) => {
189
- const providerDatasets = datas.filter(
190
- (d: Resource) => d._provider === provider.name,
191
- );
192
- return html`
193
- <div
194
- style="background: #f5f5f5; padding: 1rem; border-radius: 4px; border-left: 4px solid ${provider.color ||
195
- "#1976d2"};"
196
- >
197
- <div
198
- style="font-weight: bold; margin-bottom: 0.5rem;"
199
- >
200
- ${provider.name}
201
- </div>
202
- <div style="font-size: 1.125rem; color: #1976d2;">
203
- ${msg(str`${providerDatasets.length} datasets`)}
204
- </div>
205
- </div>
206
- `;
207
- })}
208
- </div>
209
- </div>`
210
- : nothing}
211
- <tems-catalog-filter-holder
170
+ <ds4go-catalog-filter-holder
212
171
  .displayFiltering=${false}
213
172
  @search=${this._search}
214
173
  .search=${this.search}
@@ -216,7 +175,7 @@ export class DSPCatalog extends OrbitDSPComponent {
216
175
  .objects=${datas}
217
176
  .resultCount=${this.resultCount}
218
177
  .filterCount=${this.filterCount}
219
- ></tems-catalog-filter-holder>
178
+ ></ds4go-catalog-filter-holder>
220
179
  <ds4go-catalog-data-holder
221
180
  .view=${"card"}
222
181
  .objects=${datas}
@@ -0,0 +1,307 @@
1
+ import {
2
+ formatCase,
3
+ formatDate,
4
+ OrbitComponent,
5
+ requestNavigation,
6
+ setupCacheInvalidation,
7
+ sort,
8
+ } from "@helpers";
9
+ import { Task } from "@lit/task";
10
+ import type {
11
+ Asset,
12
+ ContractAgreement,
13
+ ContractNegotiation,
14
+ OrbitComponent as OrbitComponentConfig,
15
+ PropertiesPicker,
16
+ Resource,
17
+ SearchObject,
18
+ } from "@src/component";
19
+ import type { Category, Fact } from "@src/ds4go";
20
+ import { css, html, nothing } from "lit";
21
+ import { customElement, property, state } from "lit/decorators.js";
22
+
23
+ @customElement("solid-fact-list")
24
+ export class SolidFactList extends OrbitComponent {
25
+ @property({ attribute: false })
26
+ objects?: Fact[] = [];
27
+
28
+ @state()
29
+ dspConnector: OrbitComponentConfig | undefined;
30
+
31
+ static styles = css`
32
+ .modal {
33
+ position: fixed;
34
+ top: 0;
35
+ left: 0;
36
+ right: 0;
37
+ bottom: 0;
38
+ background-color: rgba(0, 2, 49, 0.2);
39
+ z-index: 9999;
40
+ display: flex;
41
+ justify-content: center;
42
+ align-items: center;
43
+ }
44
+ `;
45
+
46
+ @property({ attribute: "header", type: String })
47
+ header?: string = "DS4GO Fact List";
48
+
49
+ @state()
50
+ search: SearchObject[] = [];
51
+
52
+ @state()
53
+ resultCount = this.objects?.length || 0;
54
+
55
+ @state()
56
+ consumerAgreements: ContractAgreement[] = [];
57
+
58
+ cherryPickedProperties: PropertiesPicker[] = [
59
+ { key: "created_at", value: "created_at", cast: formatDate },
60
+ { key: "updated_at", value: "updated_at", cast: formatDate },
61
+ { key: "ldp:contains", value: "facts" },
62
+ ];
63
+
64
+ async _afterAttach() {
65
+ setupCacheInvalidation(this, {
66
+ keywords: ["fact"],
67
+ });
68
+
69
+ // use this.dspConnector.instance to reach the connector
70
+ this.dspConnector = this.orbit?.components.find(
71
+ (c) => c.type === "dsp-connector",
72
+ );
73
+
74
+ return Promise.resolve();
75
+ }
76
+
77
+ _getResource = new Task(this, {
78
+ task: async ([objSrc]) => {
79
+ if (
80
+ !this.orbit ||
81
+ (!this.noRouter &&
82
+ this.route &&
83
+ this.currentRoute &&
84
+ !this.route.startsWith(this.currentRoute))
85
+ )
86
+ return;
87
+
88
+ if (this.dspConnector?.instance) {
89
+ // Get all data from DSP connector
90
+ await this.dspConnector.instance.loadAll();
91
+
92
+ const participantId = (this.dspConnector as any).parameters[
93
+ "participant-id"
94
+ ];
95
+ if (participantId) {
96
+ // Load agreements for all negotiations to find consumer/provider relationships
97
+ const allNegotiations = this.dspConnector.instance.negotiations || [];
98
+ const negotiationToAgreementMap = new Map<
99
+ string,
100
+ ContractAgreement
101
+ >();
102
+
103
+ const allAgreements: ContractAgreement[] = [];
104
+
105
+ await Promise.all(
106
+ allNegotiations.map(async (n: ContractNegotiation) => {
107
+ if (n.contractAgreementId) {
108
+ const agreement =
109
+ await this.dspConnector?.instance?.loadAgreement(n["@id"]);
110
+ if (agreement) {
111
+ allAgreements.push(agreement);
112
+ negotiationToAgreementMap.set(n["@id"], agreement);
113
+ }
114
+ }
115
+ }),
116
+ );
117
+
118
+ const consumerAgreements = allAgreements.filter(
119
+ (a: ContractAgreement) => {
120
+ if (!a.consumerId) return false;
121
+ const consumerParticipantId = a.consumerId.split("/").pop();
122
+ return consumerParticipantId?.startsWith(participantId);
123
+ },
124
+ );
125
+
126
+ const bundles = [];
127
+ for (const agreement of consumerAgreements) {
128
+ const asset = this.dspConnector?.instance?.assets.find(
129
+ (d: Asset) => d["@id"] === agreement.assetId,
130
+ );
131
+ const bundleUrl = asset?.dataAddress?.baseUrl;
132
+ if (bundleUrl) {
133
+ bundles.push(this._getProxyValue(bundleUrl));
134
+ }
135
+ }
136
+ const facts = Array.from(
137
+ new Set(
138
+ (await Promise.all(bundles))
139
+ .flatMap((bundle: any) => bundle?.facts || [])
140
+ .map((fact: Fact) => fact["@id"]),
141
+ ),
142
+ );
143
+
144
+ this.objects = await Promise.all(
145
+ facts.map((fact: string): Promise<Fact> => {
146
+ return this._getProxyValue(fact, true, [
147
+ { key: "created_at", value: "created_at" },
148
+ { key: "updated_at", value: "updated_at", cast: formatDate },
149
+ { key: "name", value: "name" },
150
+ { key: "description", value: "description" },
151
+ { key: "author", value: "author" },
152
+ { key: "link", value: "link" },
153
+ { key: "enclosure", value: "enclosure" },
154
+ {
155
+ key: "categories",
156
+ value: "categories",
157
+ expand: true,
158
+ cast: async (c: Resource) =>
159
+ await Promise.all(
160
+ (await c._originalResource?.["ldp:contains"]).map(
161
+ async (c: Category) => ({"@id": c["@id"], name: formatCase((await c["name"]) || "")}),
162
+ ),
163
+ ),
164
+ },
165
+ { key: "medias", value: "medias", expand: true },
166
+ { key: "url", value: "url" },
167
+ { key: "file_size", value: "file_size" },
168
+ { key: "file_type", value: "file_type" },
169
+ { key: "width", value: "width" },
170
+ { key: "height", value: "height" },
171
+ // Can't get raw review json, as store does not support datas without ids
172
+ // { key: "review", value: "review" },
173
+ ]) as Promise<Fact>;
174
+ }),
175
+ );
176
+ }
177
+
178
+ if (!Array.isArray(this.objects)) {
179
+ this.objects = [];
180
+ }
181
+
182
+ if (objSrc) {
183
+ this.object = this.objects.find((obj: Fact) => obj["@id"] === objSrc);
184
+ } else {
185
+ this.object = undefined;
186
+ }
187
+
188
+ if (this.objects.length > 0) {
189
+ return sort(this.objects, "name", "asc");
190
+ }
191
+ } else {
192
+ const listener = () => {
193
+ this.caching++;
194
+ this.requestUpdate();
195
+ document.removeEventListener("dsp-connector-ready", listener);
196
+ };
197
+ document.addEventListener("dsp-connector-ready", listener);
198
+ return;
199
+ }
200
+
201
+ return [];
202
+ },
203
+ args: () => [this.dataSrc, this.caching, this.currentRoute],
204
+ });
205
+
206
+ _search(e: Event) {
207
+ e.preventDefault();
208
+ this.search = e.detail;
209
+ this.filterCount = this.search.filter((s) => s.name !== "search").length;
210
+ }
211
+
212
+ _openModal(e: Event) {
213
+ e.preventDefault();
214
+ if (this.route) {
215
+ if ("use-id" in (this.component.routeAttributes || {})) {
216
+ this.object = e.detail;
217
+ this.dataSrc = e.detail["@id"];
218
+ requestNavigation(this.route, e.detail["@id"]);
219
+ } else {
220
+ const rdfType = e.detail["@type"]?.at(-1) ?? e.detail["@type"];
221
+ if (rdfType) {
222
+ const compatibleComponents = window.orbit?.components?.filter(
223
+ (c) => c?.routeAttributes?.["rdf-type"] === rdfType,
224
+ );
225
+
226
+ if (compatibleComponents?.[0]?.route) {
227
+ requestNavigation(compatibleComponents[0]?.route, e.detail["@id"]);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ _closeModal(e: Event) {
235
+ e.preventDefault();
236
+ if (this.route) {
237
+ requestNavigation(this.route, this.defaultDataSrc);
238
+ this.dataSrc = undefined;
239
+ this.object = undefined;
240
+ }
241
+ }
242
+
243
+ _closeModalFromBackground(e: Event) {
244
+ e.preventDefault();
245
+ if (this.route && e.target?.classList.contains("modal")) {
246
+ requestNavigation(this.route, this.defaultDataSrc);
247
+ this.dataSrc = undefined;
248
+ this.object = undefined;
249
+ }
250
+ }
251
+
252
+ _resultCountUpdate(e: Event) {
253
+ this.resultCount = e.detail ?? 0;
254
+ }
255
+
256
+ render() {
257
+ // Can't use gatekeeper as dataSrc can be none here
258
+ if (
259
+ !this.orbit ||
260
+ (!this.noRouter &&
261
+ this.route &&
262
+ this.currentRoute &&
263
+ !this.route.startsWith(this.currentRoute))
264
+ ) {
265
+ return nothing;
266
+ }
267
+ return this._getResource.render({
268
+ pending: () => html`<solid-loader></solid-loader>`,
269
+ error: (e) => {
270
+ console.warn("[solid-fact-list] Task error:", e);
271
+ return nothing;
272
+ },
273
+ complete: (datas) => {
274
+ return html`<tems-viewport>
275
+ <tems-header slot="header" heading=${this.header}></tems-header>
276
+ <div slot="content">
277
+ <ds4go-catalog-filter-holder
278
+ .displayFiltering=${this.displayFiltering}
279
+ @search=${this._search}
280
+ .search=${this.search}
281
+ .objects=${datas}
282
+ .resultCount=${this.resultCount}
283
+ .filterCount=${this.filterCount}
284
+ ></ds4go-catalog-filter-holder>
285
+ <ds4go-fact-holder
286
+ .objects=${datas}
287
+ .search=${this.search}
288
+ @clicked=${this._openModal}
289
+ @result-count=${this._resultCountUpdate}
290
+ ></ds4go-fact-holder>
291
+ ${this.object
292
+ ? html`<div
293
+ class="modal"
294
+ @click=${this._closeModalFromBackground}
295
+ >
296
+ <ds4go-fact-modal
297
+ .object=${this.object}
298
+ @close=${this._closeModal}
299
+ ></ds4go-fact-modal>
300
+ </div>`
301
+ : nothing}
302
+ </div>
303
+ </tems-viewport>`;
304
+ },
305
+ });
306
+ }
307
+ }