@unvired/turboforms-embed-sdk 2.0.60 → 2.0.62
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.
- package/dist/turboforms.esm.js +8 -8
- package/dist/turboforms.html +223 -55
- package/dist/turboforms.js +8 -8
- package/package.json +1 -1
package/dist/turboforms.html
CHANGED
|
@@ -7168,12 +7168,37 @@
|
|
|
7168
7168
|
min-width: 50px !important;
|
|
7169
7169
|
}
|
|
7170
7170
|
|
|
7171
|
-
/* 7b.
|
|
7172
|
-
.tf-embed-sdk .formio-component-datagrid .
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7171
|
+
/* 7b. Hide component labels ONLY for direct children of DataGrid table body cells (where thead header displays the column title) */
|
|
7172
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-component > label:not(.form-check-label):not(.custom-control-label),
|
|
7173
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-component > .col-form-label,
|
|
7174
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-component > [ref="label"],
|
|
7175
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-component > .control-label--hidden,
|
|
7176
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-form-group > label:not(.form-check-label):not(.custom-control-label),
|
|
7177
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-form-group > .col-form-label,
|
|
7178
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-form-group > [ref="label"],
|
|
7179
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-form-group > .control-label--hidden {
|
|
7180
|
+
display: none !important;
|
|
7181
|
+
margin: 0 !important;
|
|
7182
|
+
padding: 0 !important;
|
|
7183
|
+
height: 0 !important;
|
|
7184
|
+
min-height: 0 !important;
|
|
7185
|
+
line-height: 0 !important;
|
|
7186
|
+
font-size: 0 !important;
|
|
7187
|
+
border: none !important;
|
|
7188
|
+
visibility: hidden !important;
|
|
7189
|
+
}
|
|
7190
|
+
|
|
7191
|
+
/* For explicitly hidden labels (hideLabel: true), hide the label element */
|
|
7192
|
+
.tf-embed-sdk .formio-component-label-hidden > label.control-label--hidden,
|
|
7193
|
+
.tf-embed-sdk .formio-component-label-hidden > .control-label--hidden {
|
|
7194
|
+
display: none !important;
|
|
7195
|
+
}
|
|
7196
|
+
|
|
7197
|
+
/* Reset direct component vertical margins inside DataGrid cells for tight layout */
|
|
7198
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-form-group,
|
|
7199
|
+
.tf-embed-sdk .formio-component-datagrid table.datagrid-table > tbody > tr > td > .formio-component {
|
|
7200
|
+
margin-top: 0 !important;
|
|
7201
|
+
margin-bottom: 0 !important;
|
|
7177
7202
|
}
|
|
7178
7203
|
|
|
7179
7204
|
/* 8. Responsive Table Equality and Rescue */
|
|
@@ -7193,17 +7218,6 @@
|
|
|
7193
7218
|
.tf-embed-sdk .table td .formio-component {
|
|
7194
7219
|
width: 100% !important;
|
|
7195
7220
|
max-width: 100% !important;
|
|
7196
|
-
}
|
|
7197
|
-
|
|
7198
|
-
/* Ensure labels inside table cells don't break the responsive horizontal layout */
|
|
7199
|
-
.tf-embed-sdk .table td label:not(.custom-control-label) {
|
|
7200
|
-
max-width: 100% !important;
|
|
7201
|
-
word-break: break-word !important;
|
|
7202
|
-
white-space: normal !important;
|
|
7203
|
-
line-height: 1.1 !important;
|
|
7204
|
-
font-size: 0.75rem !important;
|
|
7205
|
-
margin-bottom: 2px !important;
|
|
7206
|
-
display: block !important;
|
|
7207
7221
|
}</style>
|
|
7208
7222
|
<style>/* TABS RENDERER - UI NORMALIZATION */
|
|
7209
7223
|
|
|
@@ -32372,7 +32386,38 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
32372
32386
|
isEntireObjectDisplay() {
|
|
32373
32387
|
return this.component.dataSrc === 'resource' && this.valueProperty === 'data';
|
|
32374
32388
|
}
|
|
32389
|
+
findMasterdataItem(val) {
|
|
32390
|
+
if (!this.component.masterdata || val == null) return null;
|
|
32391
|
+
const valStr = String(val);
|
|
32392
|
+
const valProp = this.component.valueProperty || this.valueProperty;
|
|
32393
|
+
return this.component.masterdata.find(item => {
|
|
32394
|
+
if (!item || typeof item !== 'object') return false;
|
|
32395
|
+
if (valProp) {
|
|
32396
|
+
let pVal = _.get(item, valProp);
|
|
32397
|
+
if (pVal !== undefined && pVal !== null && String(pVal) === valStr) return true;
|
|
32398
|
+
if (valProp.startsWith('data.')) {
|
|
32399
|
+
pVal = _.get(item, valProp.replace(/^data\./, ''));
|
|
32400
|
+
if (pVal !== undefined && pVal !== null && String(pVal) === valStr) return true;
|
|
32401
|
+
}
|
|
32402
|
+
if (item[valProp] !== undefined && item[valProp] !== null && String(item[valProp]) === valStr) return true;
|
|
32403
|
+
}
|
|
32404
|
+
const raw = item.data || item;
|
|
32405
|
+
for (const k of Object.keys(raw)) {
|
|
32406
|
+
if (/id$/i.test(k) || k === 'value' || k === '_id' || k === 'id') {
|
|
32407
|
+
if (raw[k] !== undefined && raw[k] !== null && String(raw[k]) === valStr) {
|
|
32408
|
+
return true;
|
|
32409
|
+
}
|
|
32410
|
+
}
|
|
32411
|
+
}
|
|
32412
|
+
return false;
|
|
32413
|
+
});
|
|
32414
|
+
}
|
|
32415
|
+
|
|
32375
32416
|
selectValueAndLabel(data) {
|
|
32417
|
+
if ((typeof data === 'string' || typeof data === 'number') && this.component.dataSrc === 'masterdata' && this.component.masterdata) {
|
|
32418
|
+
const matched = this.findMasterdataItem(data);
|
|
32419
|
+
if (matched) data = matched;
|
|
32420
|
+
}
|
|
32376
32421
|
const value = this.getOptionValue((this.isEntireObjectDisplay() && !this.itemValue(data)) ? data : this.itemValue(data));
|
|
32377
32422
|
return {
|
|
32378
32423
|
value,
|
|
@@ -32385,8 +32430,13 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
32385
32430
|
return '';
|
|
32386
32431
|
}
|
|
32387
32432
|
|
|
32433
|
+
if ((typeof data === 'string' || typeof data === 'number') && this.component.dataSrc === 'masterdata' && this.component.masterdata) {
|
|
32434
|
+
const matched = this.findMasterdataItem(data);
|
|
32435
|
+
if (matched) data = matched;
|
|
32436
|
+
}
|
|
32437
|
+
|
|
32388
32438
|
// If they wish to show the value in read only mode, then just return the itemValue here.
|
|
32389
|
-
if (this.options.readOnly && this.component.readOnlyValue) {
|
|
32439
|
+
if (this.options.readOnly && this.component.readOnlyValue && !this.component.template && !this.component.labelProperty) {
|
|
32390
32440
|
return this.itemValue(data);
|
|
32391
32441
|
}
|
|
32392
32442
|
// Perform a fast interpretation if we should not use the template.
|
|
@@ -32435,7 +32485,7 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
32435
32485
|
}
|
|
32436
32486
|
|
|
32437
32487
|
let templateData = data;
|
|
32438
|
-
if (typeof data === 'object' &&
|
|
32488
|
+
if (typeof data === 'object' && data !== null && !data.data) {
|
|
32439
32489
|
templateData = Object.assign({ data: data }, data);
|
|
32440
32490
|
}
|
|
32441
32491
|
|
|
@@ -33085,11 +33135,8 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
33085
33135
|
case 'masterdata':
|
|
33086
33136
|
logger.info(`[TF_SDK:181] ✅ masterdata case for '${this.key}'. masterdata length:`, this.filteredMasterdata ? this.filteredMasterdata.length : 0);
|
|
33087
33137
|
let safeMasterData = this.filteredMasterdata || [];
|
|
33088
|
-
if (safeMasterData.length > 0
|
|
33089
|
-
|
|
33090
|
-
if (expectsData) {
|
|
33091
|
-
safeMasterData = safeMasterData.map(item => Object.assign({ data: item }, item));
|
|
33092
|
-
}
|
|
33138
|
+
if (safeMasterData.length > 0) {
|
|
33139
|
+
safeMasterData = safeMasterData.map(item => (item && typeof item === 'object' && !item.data) ? Object.assign({ data: item }, item) : item);
|
|
33093
33140
|
}
|
|
33094
33141
|
|
|
33095
33142
|
if (!this.isFromSearch && !this.isScrollLoading && !this.component.dropdownOpened) {
|
|
@@ -34546,6 +34593,21 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
34546
34593
|
|
|
34547
34594
|
value = convertToString(value);
|
|
34548
34595
|
|
|
34596
|
+
if (this.component.dataSrc === 'masterdata' && this.component.masterdata) {
|
|
34597
|
+
const convertMasterdata = (v) => {
|
|
34598
|
+
if (typeof v === 'string' || typeof v === 'number') {
|
|
34599
|
+
const matched = this.findMasterdataItem(v);
|
|
34600
|
+
return matched || v;
|
|
34601
|
+
}
|
|
34602
|
+
return v;
|
|
34603
|
+
};
|
|
34604
|
+
if (Array.isArray(value)) {
|
|
34605
|
+
value = value.map(convertMasterdata);
|
|
34606
|
+
} else if (value != null) {
|
|
34607
|
+
value = convertMasterdata(value);
|
|
34608
|
+
}
|
|
34609
|
+
}
|
|
34610
|
+
|
|
34549
34611
|
if (['values', 'custom'].includes(this.component.dataSrc) && !this.asyncCustomValues()) {
|
|
34550
34612
|
const {
|
|
34551
34613
|
items,
|
|
@@ -34569,7 +34631,7 @@ class SmartSelectComponent extends SmartSelectField {
|
|
|
34569
34631
|
: value;
|
|
34570
34632
|
}
|
|
34571
34633
|
|
|
34572
|
-
if (_.isString(value)) {
|
|
34634
|
+
if (_.isString(value) && this.component.dataSrc !== 'masterdata') {
|
|
34573
34635
|
return value;
|
|
34574
34636
|
}
|
|
34575
34637
|
const getTemplateValue = (v) => {
|
|
@@ -39371,15 +39433,78 @@ function getValueFromData(data, path) {
|
|
|
39371
39433
|
return current;
|
|
39372
39434
|
}
|
|
39373
39435
|
|
|
39436
|
+
/**
|
|
39437
|
+
* Helper to check if a component instance has an empty value.
|
|
39438
|
+
*/
|
|
39374
39439
|
/**
|
|
39375
39440
|
* Helper to check if a component instance has an empty value.
|
|
39376
39441
|
*/
|
|
39377
39442
|
function isComponentEmpty(inst) {
|
|
39378
39443
|
if (!inst) return true;
|
|
39379
|
-
|
|
39380
|
-
|
|
39381
|
-
|
|
39382
|
-
|
|
39444
|
+
let val = inst.getValue ? inst.getValue() : inst.dataValue;
|
|
39445
|
+
|
|
39446
|
+
if (val === undefined || val === null || val === "") return true;
|
|
39447
|
+
|
|
39448
|
+
if (Array.isArray(val)) {
|
|
39449
|
+
if (val.length === 0) return true;
|
|
39450
|
+
const hasValidItem = val.some(item => {
|
|
39451
|
+
if (item === undefined || item === null || item === "") return false;
|
|
39452
|
+
if (typeof item === "object" && Object.keys(item).length === 0) return false;
|
|
39453
|
+
return true;
|
|
39454
|
+
});
|
|
39455
|
+
if (!hasValidItem) return true;
|
|
39456
|
+
}
|
|
39457
|
+
|
|
39458
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
39459
|
+
if (Object.keys(val).length === 0) return true;
|
|
39460
|
+
const hasValidVal = Object.values(val).some(v => v !== undefined && v !== null && v !== "" && v !== false);
|
|
39461
|
+
if (!hasValidVal) return true;
|
|
39462
|
+
}
|
|
39463
|
+
|
|
39464
|
+
if (inst.isEmpty) {
|
|
39465
|
+
return inst.isEmpty(val);
|
|
39466
|
+
}
|
|
39467
|
+
|
|
39468
|
+
return false;
|
|
39469
|
+
}
|
|
39470
|
+
|
|
39471
|
+
/**
|
|
39472
|
+
* Helper to retrieve the exact component instance associated with an error,
|
|
39473
|
+
* including specific row instances inside DataGrids/EditGrids.
|
|
39474
|
+
*/
|
|
39475
|
+
function getComponentByError(form, err) {
|
|
39476
|
+
if (!form || !err) return null;
|
|
39477
|
+
|
|
39478
|
+
// 1. Direct component instance reference on error object
|
|
39479
|
+
if (err.component && err.component.element) {
|
|
39480
|
+
return err.component;
|
|
39481
|
+
}
|
|
39482
|
+
|
|
39483
|
+
// 2. Try exact path (e.g., "dataGrid[0].crewLead", "dataGrid.0.crewLead")
|
|
39484
|
+
const path = err.path || err.context?.path || err.context?.hasPath;
|
|
39485
|
+
if (path) {
|
|
39486
|
+
let inst = typeof form.getComponent === "function" ? form.getComponent(path) : null;
|
|
39487
|
+
if (inst && inst.element) return inst;
|
|
39488
|
+
|
|
39489
|
+
// Convert array notation dataGrid[0].crewLead -> dataGrid.0.crewLead
|
|
39490
|
+
const dotPath = String(path).replace(/\[(\d+)\]/g, ".$1");
|
|
39491
|
+
inst = typeof form.getComponent === "function" ? form.getComponent(dotPath) : null;
|
|
39492
|
+
if (inst && inst.element) return inst;
|
|
39493
|
+
|
|
39494
|
+
// Convert dot notation dataGrid.0.crewLead -> dataGrid[0].crewLead
|
|
39495
|
+
const bracketPath = String(path).replace(/\.(\d+)\./g, "[$1].");
|
|
39496
|
+
inst = typeof form.getComponent === "function" ? form.getComponent(bracketPath) : null;
|
|
39497
|
+
if (inst && inst.element) return inst;
|
|
39498
|
+
}
|
|
39499
|
+
|
|
39500
|
+
// 3. Fallback to component key
|
|
39501
|
+
const key = err.context?.component?.key || err.component?.key || (typeof err.context?.component === "string" ? err.context.component : null);
|
|
39502
|
+
if (key && typeof form.getComponent === "function") {
|
|
39503
|
+
const inst = form.getComponent(key);
|
|
39504
|
+
if (inst && inst.element) return inst;
|
|
39505
|
+
}
|
|
39506
|
+
|
|
39507
|
+
return null;
|
|
39383
39508
|
}
|
|
39384
39509
|
|
|
39385
39510
|
/**
|
|
@@ -39392,10 +39517,44 @@ function clearComponentErrorUI(inst) {
|
|
|
39392
39517
|
inst.setCustomValidity("");
|
|
39393
39518
|
}
|
|
39394
39519
|
inst.element.classList.remove("has-error", "formio-error-wrapper", "is-invalid");
|
|
39395
|
-
|
|
39396
|
-
|
|
39397
|
-
|
|
39398
|
-
|
|
39520
|
+
|
|
39521
|
+
// Only remove .is-invalid from direct choice or input elements belonging to this component
|
|
39522
|
+
const choices = inst.element.querySelectorAll(".choices__inner, .ui.selection.dropdown, .is-invalid");
|
|
39523
|
+
choices.forEach((el) => {
|
|
39524
|
+
if (el.closest(".formio-component") === inst.element) {
|
|
39525
|
+
el.classList.remove("is-invalid");
|
|
39526
|
+
}
|
|
39527
|
+
});
|
|
39528
|
+
|
|
39529
|
+
// Clear error message container ONLY if it belongs directly to inst.element (not a child component)
|
|
39530
|
+
const errorLabels = inst.element.querySelectorAll(".formio-errors, .invalid-feedback, [ref='messageContainer']");
|
|
39531
|
+
errorLabels.forEach((errorLabel) => {
|
|
39532
|
+
if (errorLabel.closest(".formio-component") === inst.element) {
|
|
39533
|
+
errorLabel.innerHTML = "";
|
|
39534
|
+
errorLabel.style.display = "none";
|
|
39535
|
+
}
|
|
39536
|
+
});
|
|
39537
|
+
}
|
|
39538
|
+
|
|
39539
|
+
/**
|
|
39540
|
+
* Scans all component instances in the form and clears error UI for any component that has been filled.
|
|
39541
|
+
*/
|
|
39542
|
+
function clearResolvedErrors(form) {
|
|
39543
|
+
if (!form || typeof form.everyComponent !== "function") return;
|
|
39544
|
+
form.everyComponent((inst) => {
|
|
39545
|
+
if (!inst || !inst.element) return;
|
|
39546
|
+
const compType = inst.component?.type || inst.type;
|
|
39547
|
+
const isContainerType = ["datagrid", "editgrid", "container", "panel", "columns", "fieldset", "well", "tabs", "form"].includes(compType);
|
|
39548
|
+
if (isContainerType) return;
|
|
39549
|
+
|
|
39550
|
+
// Check if this component currently displays error styling
|
|
39551
|
+
const hasErrorClass = inst.element.classList.contains("has-error") || inst.element.classList.contains("formio-error-wrapper");
|
|
39552
|
+
const hasVisibleError = Array.from(inst.element.querySelectorAll(".formio-errors, .invalid-feedback"))
|
|
39553
|
+
.some(el => el.style.display !== "none" && el.innerHTML.trim() !== "" && el.closest(".formio-component") === inst.element);
|
|
39554
|
+
|
|
39555
|
+
if ((hasErrorClass || hasVisibleError) && !isComponentEmpty(inst)) {
|
|
39556
|
+
clearComponentErrorUI(inst);
|
|
39557
|
+
}
|
|
39399
39558
|
});
|
|
39400
39559
|
}
|
|
39401
39560
|
|
|
@@ -39406,13 +39565,17 @@ function showComponentErrorUI(inst, err) {
|
|
|
39406
39565
|
if (!inst || !inst.element) return;
|
|
39407
39566
|
inst.element.classList.add("has-error", "formio-error-wrapper");
|
|
39408
39567
|
const choices = inst.element.querySelector(".choices__inner, .ui.selection.dropdown");
|
|
39409
|
-
if (choices
|
|
39568
|
+
if (choices && choices.closest(".formio-component") === inst.element) {
|
|
39569
|
+
choices.classList.add("is-invalid");
|
|
39570
|
+
}
|
|
39410
39571
|
|
|
39411
39572
|
const msg = err.message || (err.context?.component?.label ? `${err.context.component.label} is required` : `${inst.component?.label || inst.key} is required`);
|
|
39412
39573
|
const compId = inst.id || inst.component?.id || inst.key;
|
|
39413
39574
|
const innerErrorId = `e-${compId}-${inst.key}`;
|
|
39414
39575
|
|
|
39415
|
-
let errorLabel = inst.element.
|
|
39576
|
+
let errorLabel = Array.from(inst.element.querySelectorAll(".formio-errors, [ref='messageContainer']"))
|
|
39577
|
+
.find(el => el.closest(".formio-component") === inst.element);
|
|
39578
|
+
|
|
39416
39579
|
if (!errorLabel) {
|
|
39417
39580
|
errorLabel = document.createElement("div");
|
|
39418
39581
|
errorLabel.setAttribute("ref", "messageContainer");
|
|
@@ -39438,13 +39601,8 @@ function setupOnChange(privateExternal, permission) {
|
|
|
39438
39601
|
document.activeElement.blur();
|
|
39439
39602
|
}
|
|
39440
39603
|
|
|
39441
|
-
// Immediately clear error UI for any component
|
|
39442
|
-
|
|
39443
|
-
const inst = event.changed.instance;
|
|
39444
|
-
if (!isComponentEmpty(inst)) {
|
|
39445
|
-
clearComponentErrorUI(inst);
|
|
39446
|
-
}
|
|
39447
|
-
}
|
|
39604
|
+
// Immediately clear error UI for any filled component across the form (including DataGrid rows)
|
|
39605
|
+
clearResolvedErrors(formObj);
|
|
39448
39606
|
|
|
39449
39607
|
// Clear existing timeout
|
|
39450
39608
|
if (changeTimeout) {
|
|
@@ -39597,21 +39755,31 @@ function FormOnComplete() {
|
|
|
39597
39755
|
let isTrulyInvalid = false;
|
|
39598
39756
|
let trueErrors = [];
|
|
39599
39757
|
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
|
|
39603
|
-
|
|
39604
|
-
|
|
39605
|
-
|
|
39606
|
-
|
|
39607
|
-
|
|
39608
|
-
|
|
39609
|
-
|
|
39610
|
-
|
|
39611
|
-
|
|
39758
|
+
// Combine errors from formObj.errors, error (promise reject), error.errors, error.details
|
|
39759
|
+
let rawErrors = [];
|
|
39760
|
+
if (formObj && Array.isArray(formObj.errors) && formObj.errors.length > 0) {
|
|
39761
|
+
rawErrors = rawErrors.concat(formObj.errors);
|
|
39762
|
+
}
|
|
39763
|
+
if (Array.isArray(error)) {
|
|
39764
|
+
rawErrors = rawErrors.concat(error);
|
|
39765
|
+
} else if (error && Array.isArray(error.errors)) {
|
|
39766
|
+
rawErrors = rawErrors.concat(error.errors);
|
|
39767
|
+
} else if (error && Array.isArray(error.details)) {
|
|
39768
|
+
rawErrors = rawErrors.concat(error.details);
|
|
39769
|
+
} else if (error && typeof error === "object" && (error.message || error.context)) {
|
|
39770
|
+
rawErrors.push(error);
|
|
39771
|
+
}
|
|
39772
|
+
|
|
39773
|
+
if (rawErrors.length > 0) {
|
|
39774
|
+
rawErrors.forEach((err) => {
|
|
39775
|
+
const inst = getComponentByError(formObj, err);
|
|
39776
|
+
if (inst && inst.element) {
|
|
39777
|
+
if (!isComponentEmpty(inst)) {
|
|
39778
|
+
clearComponentErrorUI(inst);
|
|
39612
39779
|
} else {
|
|
39613
39780
|
isTrulyInvalid = true;
|
|
39614
39781
|
trueErrors.push(err);
|
|
39782
|
+
showComponentErrorUI(inst, err);
|
|
39615
39783
|
}
|
|
39616
39784
|
} else {
|
|
39617
39785
|
isTrulyInvalid = true;
|
|
@@ -41474,7 +41642,7 @@ initThemeHook();
|
|
|
41474
41642
|
</div>
|
|
41475
41643
|
|
|
41476
41644
|
<div id="sticky-footer">
|
|
41477
|
-
<div class="build-version">SDK v2.0.
|
|
41645
|
+
<div class="build-version">SDK v2.0.62</div>
|
|
41478
41646
|
|
|
41479
41647
|
<button class="footer-btn footer-btn-secondary" id="prevBtn" style="display:none" onclick="FormOnPrevious()">
|
|
41480
41648
|
<i class="bi bi-chevron-left"></i> Previous
|