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
89 changes: 89 additions & 0 deletions src/collectionFieldUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const COLLECTION_DATA_PREFIX = "data.";

export function normalizeCollectionFieldPath(fieldPath) {
if (typeof fieldPath !== "string") {
return fieldPath;
}

return fieldPath.startsWith(COLLECTION_DATA_PREFIX)
? fieldPath.slice(COLLECTION_DATA_PREFIX.length)
: fieldPath;
}

export function getCollectionFieldOptions(collection = {}) {
const options = [];
const values = new Set();

const addOption = ({ text, value }) => {
const normalizedValue = normalizeCollectionFieldPath(value);

if (!normalizedValue || values.has(normalizedValue)) {
return;
}

values.add(normalizedValue);
options.push({
text: text || normalizedValue,
value: normalizedValue
});
};

if (Array.isArray(collection?.fields) && collection.fields.length > 0) {
collection.fields.forEach((field) => {
if (!field) {
return;
}

addOption({
text: field.text || field.value,
value: field.value
});
});
} else {
const [firstRecord] = collection?.dataRecordList || [];

if (firstRecord?.data) {
Object.keys(firstRecord.data).forEach((field) => {
addOption({ text: field, value: field });
});
}
}

addOption({ text: "id", value: "id" });

return options;
}

export function mapCollectionRecordData(data = {}, options = []) {
if (!data || typeof data !== "object" || Array.isArray(data)) {
return {};
}

if (!Array.isArray(options) || options.length === 0) {
return { ...data };
}

const mappedData = {};

options.forEach((option) => {
if (!option) {
return;
}

const targetKey = normalizeCollectionFieldPath(
option.key || option.content
);
const sourceKey = [option.content, option.key]
.map(normalizeCollectionFieldPath)
.find(
(candidate) =>
candidate && Object.prototype.hasOwnProperty.call(data, candidate)
);

if (sourceKey && targetKey) {
mappedData[targetKey] = data[sourceKey];
}
});

return mappedData;
}
23 changes: 2 additions & 21 deletions src/components/inspector/collection-data-source.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<script>
import { cloneDeep } from "lodash";
import CollectionRecordsList from "./collection-records-list.vue";
import { getCollectionFieldOptions } from "../../collectionFieldUtils";

const CONFIG_FIELDS = [
"collectionFields",
Expand Down Expand Up @@ -185,27 +186,7 @@ export default {
}
},
getCollectionColumns(records) {
this.singleFieldOptions = [];

// Prefer the collection schema when CollectionRecordsList has
// forwarded it on the v-model payload.
if (Array.isArray(records?.fields) && records.fields.length > 0) {
records.fields.forEach((field) => {
this.singleFieldOptions.push({
text: field.text || field.value,
value: field.value
});
});
return;
}

// Fallback: get columns from the first record's populated keys.
const [firstRecord] = records?.dataRecordList || [];
if (firstRecord?.data) {
for (const [key] of Object.entries(firstRecord.data)) {
this.singleFieldOptions.push({ text: key, value: key });
}
}
this.singleFieldOptions = getCollectionFieldOptions(records);
}
}
};
Expand Down
26 changes: 5 additions & 21 deletions src/components/inspector/column-setup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ import draggable from 'vuedraggable';
import { dataSources, dataSourceValues } from './data-source-types';
import MonacoEditor from 'vue-monaco';
import { cloneDeep } from "lodash";
import { getCollectionFieldOptions } from "../../collectionFieldUtils";

export default {
components: {
Expand Down Expand Up @@ -394,27 +395,10 @@ export default {
}
},
getCollectionColumns(collection) {
this.collectionOptions = [{ text: "All columns", value: "all" }];

// Prefer the collection schema when CollectionRecordsList
// has forwarded it on the v-model payload.
if (Array.isArray(collection?.fields) && collection.fields.length > 0) {
collection.fields.forEach((field) => {
this.collectionOptions.push({
text: field.text || field.value,
value: field.value
});
});
return;
}

// Fallback: get columns from the first record's populated keys.
const [firstRecord] = collection?.dataRecordList || [];
if (firstRecord?.data) {
for (const [key] of Object.entries(firstRecord.data)) {
this.collectionOptions.push({ text: key, value: key });
}
}
this.collectionOptions = [
{ text: "All columns", value: "all" },
...getCollectionFieldOptions(collection)
];
},
initData() {
this.dataSource = this.options.dataSource;
Expand Down
50 changes: 24 additions & 26 deletions src/components/renderer/form-record-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ 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 {
mapCollectionRecordData,
normalizeCollectionFieldPath
} from "../../collectionFieldUtils";

const jsonOptionsActionsColumn = {
key: "__actions",
Expand Down Expand Up @@ -581,7 +585,10 @@ export default {
onRadioChange(selectedItem, index) {
const globalIndex = (this.currentPage - 1) * this.perPage + index;
if(this.source?.singleField) {
let valueOfColumn = selectedItem[this.source.singleField];
const singleField = normalizeCollectionFieldPath(
this.source.singleField
);
const valueOfColumn = selectedItem[singleField];
this.componentOutput(valueOfColumn);
} else {
selectedItem = { ...selectedItem, selectedRowIndex: globalIndex};
Expand Down Expand Up @@ -728,27 +735,14 @@ export default {

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

changeCollectionColumns(collectionFieldsColumns, columnsSelected) {
const optionsList = columnsSelected.optionsList;
const mappedColumns = collectionFieldsColumns.map((column) => ({
...column,
data: mapCollectionRecordData(column.data, optionsList)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapCollectionRecordData only reads keys from column.data, but Collection API records expose the system id on the parent object ({ id, data: { ...fields } }), not inside data. After this PR always offers id as a column, Preview/table cells for that column stay empty for real Collection payloads.

The Cypress fixture masks this by putting id inside data ({ id: recordData.id, data: recordData } where recordData also contains id).

Impact: Users can select the id column (or All columns including id), but Preview and runtime Record Lists will show blank id values against real /collections/{id}/records responses.

Suggested Fix: Before mapping, merge the record-level id into the payload, e.g. mapCollectionRecordData({ ...(column.data || {}), ...(column.id != null ? { id: column.id } : {}) }, optionsList). Also update the Cypress intercept so data does not include id, and assert Preview still renders the parent record id.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CarliPinell Thanks for flagging this.

I verified the current Collection API contract: CollectionsApiCollection explicitly copies the parent record ID into data.id before returning records (source). I also confirmed that a real /api/1.0/collections/2/records response contains matching IDs at both record.id and record.data.id, so the Cypress fixture mirrors the production payload and Preview can map id from column.data. Could you share the endpoint or payload where id exists only on the parent? If that shape exists, I can add compatibility coverage; otherwise, no code change should be needed here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! thanks.

}));

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

Object.keys(dataObject).forEach(dataKey => {
const matchingOption = optionsList.find(option => option.content === dataKey);

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

column.data = newDataObject;
});

this.setCollectionIntoList(collectionFieldsColumns);

this.setCollectionIntoList(mappedColumns);
},
setCollectionIntoList(arrayCollection) {
const result = [];
Expand Down Expand Up @@ -825,7 +819,10 @@ export default {

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);
const singleField = normalizeCollectionFieldPath(
this.source.singleField
);
const match = rows.find(row => row[singleField] === this.value);
if (match) {
this.selectedRow = match;
}
Expand Down Expand Up @@ -915,17 +912,18 @@ export default {
const { jsonData, key, value, dataName } = this.fields;

let convertToVuetableFormat = {};
if(this.source?.sourceOptions === "Collection") {
convertToVuetableFormat = (option) => {
if (this.source?.sourceOptions === "Collection") {
convertToVuetableFormat = (option) => {
const keyValue = normalizeCollectionFieldPath(option[key || "key"]);
return {
key: option[key || "key"],
key: keyValue,
sortable: true,
label: option.label || option[key || "key"],
label: option.label || keyValue,
tdClass: "table-column"
};
};
} else {
convertToVuetableFormat = (option) => {
convertToVuetableFormat = (option) => {
return {
key: option[key || "value"],
sortable: true,
Expand Down
Loading
Loading