Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions src/components/renderer/form-record-list-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Pure helpers for FormRecordList collection radio/checkbox selection.
* Kept outside the Vue SFC so unit tests can exercise production logic
* without mounting the heavy FormRecordList component tree.
*/

export function isSingleFieldSelectionMode(source) {
return (
source?.dataSelectionOptions === "single-field" ||
(source?.dataSelectionOptions == null && !!source?.singleField)
);
}

/**
* Resolve the configured singleField value from a collection row.
* Rows are remapped from collection field names (content) to column keys,
* so singleField may not match Object.keys(row) directly.
*/
export function getSingleFieldValue(selectedItem, source, fields) {
const field = source?.singleField;
if (!field || !selectedItem || typeof selectedItem !== "object") {
return undefined;
}

if (Object.hasOwn(selectedItem, field)) {
return selectedItem[field];
}

const optionsList = fields?.optionsList || [];
const byContent = optionsList.find((opt) => opt.content === field);
if (byContent && Object.hasOwn(selectedItem, byContent.key)) {
return selectedItem[byContent.key];
}

const byKey = optionsList.find((opt) => opt.key === field);
if (byKey && Object.hasOwn(selectedItem, byKey.key)) {
return selectedItem[byKey.key];
}

const lower = String(field).toLowerCase();
const matchedKey = Object.keys(selectedItem).find(
(key) => String(key).toLowerCase() === lower
);
return matchedKey ? selectedItem[matchedKey] : undefined;
}

export function rowMatchesSingleFieldValue(row, value, source, fields) {
return getSingleFieldValue(row, source, fields) === value;
}

/**
* Convert a b-table page-relative cell index into a global row index.
* @change already provides a page-relative index; do not pass a global
* index here or the page offset will be applied twice.
*/
export function toGlobalRowIndex(pageRelativeIndex, currentPage, perPage) {
return (currentPage - 1) * perPage + pageRelativeIndex;
}

/**
* Build the value emitted for a radio selection.
* Returns undefined for missing single-field values (caller should not emit).
*/
export function buildRadioSelectionValue(
selectedItem,
pageRelativeIndex,
currentPage,
perPage,
source,
fields
) {
if (isSingleFieldSelectionMode(source) && source?.singleField) {
return getSingleFieldValue(selectedItem, source, fields);
}

return {
...selectedItem,
selectedRowIndex: toGlobalRowIndex(
pageRelativeIndex,
currentPage,
perPage
)
};
}

export function getCollectionRowKey(item) {
if (!item || typeof item !== "object") {
return null;
}

const entries = Object.entries(item).filter(
([key]) => key !== "selectedRowsIndex" && key !== "selectedRowIndex"
);

if (entries.length === 0) {
return null;
}

entries.sort(([keyA], [keyB]) => {
if (keyA > keyB) return 1;
if (keyA < keyB) return -1;
return 0;
});
return JSON.stringify(entries);
}

export function findSingleRecordRadioMatch(value, rows) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}

const valueKey = getCollectionRowKey(value);
if (valueKey) {
const byContent = rows.find(
(row) => getCollectionRowKey(row) === valueKey
);
if (byContent) {
return byContent;
}
}

const idx = value.selectedRowIndex;
if (idx != null && idx >= 0 && idx < rows.length) {
return rows[idx];
}
return null;
}

export function findRadioSelectionMatch(value, rows, source, fields) {
if (value == null || value === "" || !Array.isArray(rows) || rows.length === 0) {
return null;
}

if (isSingleFieldSelectionMode(source) && source?.singleField) {
return (
rows.find((row) =>
rowMatchesSingleFieldValue(row, value, source, fields)
) || null
);
}

if (typeof value === "object" && !Array.isArray(value)) {
return findSingleRecordRadioMatch(value, rows);
}

return null;
}

/**
* Remap collection API rows from field content names to column keys,
* always preserving the configured singleField under its original name.
*/
export function remapCollectionRowData(dataObject, optionsList, singleField) {
const sourceData = dataObject || {};
const newDataObject = {};

Object.keys(sourceData).forEach((dataKey) => {
const matchingOption = (optionsList || []).find(
(option) => option.content === dataKey
);
if (matchingOption) {
newDataObject[matchingOption.key] = sourceData[dataKey];
}
});

if (singleField && Object.hasOwn(sourceData, singleField)) {
newDataObject[singleField] = sourceData[singleField];
}

return newDataObject;
}
156 changes: 85 additions & 71 deletions src/components/renderer/form-record-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ import VueFormRenderer from "@/components/vue-form-renderer.vue";
import mustacheEvaluation from "../../mixins/mustacheEvaluation";
import MustacheHelper from "../inspector/mustache-helper.vue";
import Mustache from "mustache";
import {
buildRadioSelectionValue,
findRadioSelectionMatch,
getCollectionRowKey,
remapCollectionRowData
} from "./form-record-list-selection";

const jsonOptionsActionsColumn = {
key: "__actions",
Expand Down Expand Up @@ -474,12 +480,15 @@ export default {
},
// Watch for changes in validationData to handle any Mustache variable changes
validationData: {
handler(newValue, oldValue) {
if (this.source?.sourceOptions === "Collection" && this.source?.collectionFields?.pmql) {
this.onCollectionChange(
this.source?.collectionFields?.collectionId,
this.source?.collectionFields?.pmql
);
handler() {
if (this.source?.sourceOptions === "Collection") {
const pmql = this.getCollectionPmql();
if (pmql) {
this.onCollectionChange(
this.source?.collectionFields?.collectionId,
pmql
);
}
}
},
deep: true,
Expand All @@ -500,7 +509,10 @@ export default {
}

if(this.source?.sourceOptions === "Collection") {
this.onCollectionChange(this.source?.collectionFields?.collectionId, this.source?.collectionFields?.pmql);
this.onCollectionChange(
this.source?.collectionFields?.collectionId,
this.getCollectionPmql()
);
}

this.setStyleMode(this.designerMode?.designerOptions);
Expand Down Expand Up @@ -576,17 +588,53 @@ export default {
}
},
componentOutput(data) {
this.$emit('input', data);
// Avoid emitting undefined, which would leave the bound variable as null
// and lose the selection when submitting to the next task.
if (typeof data === "undefined") {
return;
}
this.$emit("input", data);
// Also write directly to validationData (screen vdata). Submit reads vdata, and
// v-model/@input listener order can drop object selections before they sync.
this.persistValueToFormData(data);
},
onRadioChange(selectedItem, index) {
const globalIndex = (this.currentPage - 1) * this.perPage + index;
if(this.source?.singleField) {
let valueOfColumn = selectedItem[this.source.singleField];
this.componentOutput(valueOfColumn);
} else {
selectedItem = { ...selectedItem, selectedRowIndex: globalIndex};
this.componentOutput(selectedItem);
persistValueToFormData(data) {
if (
!this.name ||
!this.validationData ||
typeof this.validationData !== "object"
) {
return;
}
if (String(this.name).includes(".")) {
_.set(this.validationData, this.name, data);
const rootKey = String(this.name).split(".")[0];
this.$set(this.validationData, rootKey, this.validationData[rootKey]);
return;
}
this.$set(this.validationData, this.name, data);
},
getCollectionPmql() {
// PMQL can live on source.pmql and/or source.collectionFields.pmql depending
// on how the inspector synced the config; prefer the nested copy when present.
const nestedPmql = this.source?.collectionFields?.pmql;
if (typeof nestedPmql === "string") {
return nestedPmql;
}
return this.source?.pmql || "";
},
onRadioChange(selectedItem, pageRelativeIndex) {
// b-table cell slot `index` is page-relative; convert once here.
this.componentOutput(
buildRadioSelectionValue(
selectedItem,
pageRelativeIndex,
this.currentPage,
this.perPage,
this.source,
this.fields
)
);
},
onMultipleSelectionChange(selIndex) {
this.collectionData.forEach((item, index) => {
Expand Down Expand Up @@ -725,30 +773,21 @@ export default {
.catch(() => {
this.collectionData = [];
});

this.$emit("change", this.field);
},
changeCollectionColumns(collectionFieldsColumns,columnsSelected) {

const optionsList = columnsSelected.optionsList;

collectionFieldsColumns.forEach(column => {
let dataObject = column.data;
let newDataObject = {};

Object.keys(dataObject).forEach(dataKey => {
const matchingOption = optionsList.find(option => option.content === dataKey);
changeCollectionColumns(collectionFieldsColumns, columnsSelected) {
const optionsList = columnsSelected?.optionsList || [];
const singleField = this.source?.singleField;

if (matchingOption) {
newDataObject[matchingOption.key] = dataObject[dataKey];
}
});

column.data = newDataObject;
collectionFieldsColumns.forEach((column) => {
// eslint-disable-next-line no-param-reassign
column.data = remapCollectionRowData(
column.data,
optionsList,
singleField
);
});

this.setCollectionIntoList(collectionFieldsColumns);

this.setCollectionIntoList(collectionFieldsColumns);
},
setCollectionIntoList(arrayCollection) {
const result = [];
Expand Down Expand Up @@ -819,26 +858,18 @@ export default {
// Restore selectedRow after collection data (re)loads or when value prop changes.
// Mirrors reapplyCollectionSelections for the single-record (radio) case.
restoreRadioSelection(rows) {
if (!this.value || !Array.isArray(rows) || rows.length === 0) {
return;
}

if (this.source?.singleField) {
// singleField mode emits a scalar; find the row whose field matches
const match = rows.find(row => row[this.source.singleField] === this.value);
if (match) {
this.selectedRow = match;
}
} else if (typeof this.value === "object" && !Array.isArray(this.value)) {
// Regular single-record mode emits { ...item, selectedRowIndex: N }
const idx = this.value.selectedRowIndex;
if (idx != null && idx >= 0 && idx < rows.length) {
this.selectedRow = rows[idx];
}
const match = findRadioSelectionMatch(
this.value,
rows,
this.source,
this.fields
);
if (match) {
this.selectedRow = match;
}
},
shouldPersistCollectionSelection() {
const pmql = this.source?.collectionFields?.pmql;
const pmql = this.getCollectionPmql();
return (
this.source?.sourceOptions === "Collection" &&
this.source?.dataSelectionOptions === "multiple-records" &&
Expand All @@ -847,24 +878,7 @@ export default {
);
},
getCollectionRowKey(item) {
if (!item || typeof item !== "object") {
return null;
}

const entries = Object.entries(item).filter(
([key]) => key !== "selectedRowsIndex"
);

if (entries.length === 0) {
return null;
}

entries.sort(([keyA], [keyB]) => {
if (keyA > keyB) return 1;
if (keyA < keyB) return -1;
return 0;
});
return JSON.stringify(entries);
return getCollectionRowKey(item);
},
updateRowDataNamePrefix() {
this.setUploadDataNamePrefix(this.currentRowIndex);
Expand Down
Loading
Loading