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
5 changes: 4 additions & 1 deletion src/components/inspector/collection-data-source.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default {
components: {
CollectionRecordsList
},
props: ["value", "screenType"],
props: ["value", "screenType", "selectedControl"],
data() {
return {
fields: [],
Expand Down Expand Up @@ -180,6 +180,9 @@ export default {
},
methods: {
displayOptionChange() {
if (this.sourceOptions === "Collection" && this.selectedControl?.config) {
this.$set(this.selectedControl.config, "editable", false);
}
this.collectionFields = [];
this.collectionFieldsColumns = [];
this.pmql = null;
Expand Down
22 changes: 13 additions & 9 deletions src/components/renderer/form-record-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</div>
<div v-if="styleMode === 'Classic'" class="col text-right">
<button
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
class="btn btn-primary"
data-cy="add-row"
@click="showAddForm"
Expand Down Expand Up @@ -82,7 +82,7 @@

</template>
<template #cell(__actions)="{ index, item }">
<div class="actions">
<div v-if="canEditRecords && !selfReferenced" class="actions">
<div
class="btn-group btn-group-sm"
role="group"
Expand Down Expand Up @@ -148,7 +148,7 @@
</div>
<div v-if="styleMode === 'Modern'" class="col text-right">
<button
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
class="btn btn-link text-primary class-button-modern"
data-cy="add-row"
@click="showAddForm"
Expand All @@ -160,7 +160,7 @@
</template>

<b-modal
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
ref="addModal"
:static="true"
size="lg"
Expand Down Expand Up @@ -188,7 +188,7 @@
/>
</b-modal>
<b-modal
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
ref="editModal"
:static="true"
size="lg"
Expand Down Expand Up @@ -216,7 +216,7 @@
/>
</b-modal>
<b-modal
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
ref="deleteModal"
size="lg"
:ok-title="$t('Delete')"
Expand All @@ -229,7 +229,7 @@
<p>{{ $t("Are you sure you want to remove this record?") }}</p>
</b-modal>
<b-modal
v-if="editable && !selfReferenced"
v-if="canEditRecords && !selfReferenced"
ref="infoModal"
size="sm"
:ok-title="$t('OK')"
Expand All @@ -242,7 +242,7 @@
<p>{{ $t("The form to be displayed is not assigned.") }}</p>
</b-modal>

<div v-if="editable && selfReferenced" class="alert alert-danger">
<div v-if="canEditRecords && selfReferenced" class="alert alert-danger">
{{
$t(
"The Record List control is not allowed to reference other controls on its own page to add or edit records. Specify a secondary page with controls to enter records."
Expand Down Expand Up @@ -340,6 +340,10 @@ export default {
};
},
computed: {
canEditRecords() {
const sourceOptions = this.source?.sourceOptions ?? "Variable";
return this.editable === true && sourceOptions === "Variable";
},
tableClassModern() {
if (this.styleMode === 'Modern') {
switch (this.bgcolormodern) {
Expand Down Expand Up @@ -430,7 +434,7 @@ export default {
tableFields() {
const fields = this.getTableFieldsFromDataSource();

if (this.editable && !this.selfReferenced) {
if (this.canEditRecords && !this.selfReferenced) {
fields.push(jsonOptionsActionsColumn);
}

Expand Down
126 changes: 126 additions & 0 deletions tests/unit/RecordListSource.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { mount, shallowMount } from '@vue/test-utils';
import { BTable, BFormSelect } from 'bootstrap-vue';
import RecordList from '@/components/renderer/form-record-list.vue';
import CollectionDataSource from '@/components/inspector/collection-data-source.vue';

jest.mock('@/components/vue-form-renderer.vue', () => ({ render: h => h('div') }));
jest.mock('@/components/inspector/mustache-helper.vue', () => ({}));
jest.mock('@/components/inspector/collection-records-list.vue', () => ({}));
jest.mock('@processmaker/vue-form-elements', () => ({ dateUtils: { formatIfDate: value => value } }));

const source = sourceOptions => ({
sourceOptions,
collectionFields: [],
collectionFieldsColumns: [],
dataSelectionOptions: 'no-selection',
});

function renderList(style, sourceOptions, editable = true, extraProps = {}) {
return mount(RecordList, {
propsData: {
source: source(sourceOptions),
editable,
fields: { jsonData: '[{"value":"name","content":"Name"}]' },
value: [{ name: 'Saved record' }],
form: 2,
formConfig: [{ items: [] }, { items: [] }, { items: [] }],
validationData: {},
designerMode: { designerOptions: style },
...extraProps,
},
parentComponent: { data: () => ({ currentPage: 1 }) },
mocks: { $t: text => text },
stubs: {
'b-table': BTable,
'b-modal': { template: '<div><slot /></div>' },
'b-pagination': true,
'vue-form-renderer': true,
},
methods: { onCollectionChange: jest.fn() },
});
}

function expectEditing(wrapper, enabled) {
['add-row', 'edit-row', 'remove-row', 'modal-add', 'modal-edit',
'modal-remove', 'modal-not-assigned'].forEach(control => {
expect(wrapper.find(`[data-cy="${control}"]`).exists()).toBe(enabled);
});
expect(wrapper.vm.tableFields.some(field => field.key === '__actions')).toBe(enabled);
}

describe.each(['Classic', 'Modern'])('Record List source in %s style', style => {
describe.each([
{ label: 'missing source', value: undefined },
{ label: 'null source', value: null },
{ label: 'empty source', value: {} },
{ label: 'null source option', value: { sourceOptions: null } },
])('legacy configuration with $label', ({ value: legacySource }) => {
test.each([true, false])('preserves editable set to %p', editable => {
const wrapper = renderList(style, 'Variable', editable, { source: legacySource });
expectEditing(wrapper, editable);
expect(wrapper.text()).toContain('Saved record');
wrapper.destroy();
});
});

test('previously saved Collection configuration does not expose Variable editing', () => {
const saved = JSON.parse(JSON.stringify({ source: source('Collection'), editable: true }));
const wrapper = renderList(style, 'Collection', true, saved);
expectEditing(wrapper, false);
expect(wrapper.text()).toContain('Saved record');
wrapper.destroy();
});

test('Variable records remain editable and react to a source change', async () => {
const wrapper = renderList(style, 'Variable');
expectEditing(wrapper, true);
await wrapper.setProps({ source: source('Collection') });
expectEditing(wrapper, false);
await wrapper.setProps({ source: source('Variable') });
expectEditing(wrapper, true);
wrapper.destroy();
});

test.each([false, 'true', 1])('Variable editing requires boolean true, received %p', editable => {
const wrapper = renderList(style, 'Variable', editable);
expectEditing(wrapper, false);
wrapper.destroy();
});

test('Collection does not display the Variable self-reference warning', () => {
const wrapper = renderList(style, 'Collection', true, { form: 1 });
expect(wrapper.vm.selfReferenced).toBe(true);
expect(wrapper.find('.alert-danger').exists()).toBe(false);
wrapper.destroy();
});

test('Variable self-reference protection remains active', () => {
const wrapper = renderList(style, 'Variable', true, { form: 1 });
expectEditing(wrapper, false);
expect(wrapper.find('.alert-danger').exists()).toBe(true);
wrapper.destroy();
});
});

describe('Record List source inspector', () => {
test('switching to Collection persists editable false after saving and reopening', async () => {
const selectedControl = { config: { source: source('Variable'), editable: true, form: 1 } };
const wrapper = shallowMount(CollectionDataSource, {
propsData: { value: selectedControl.config.source, selectedControl },
mocks: { $t: text => text },
stubs: { 'b-form-select': BFormSelect },
});
wrapper.vm.$on('input', value => { selectedControl.config.source = value; });
await wrapper.find('[data-cy="inspector-collection-data-source"]').setValue('Collection');
await wrapper.vm.$nextTick();
const reopened = JSON.parse(JSON.stringify(selectedControl));
expect(reopened.config.editable).toBe(false);
expect(reopened.config.source.sourceOptions).toBe('Collection');
expect(reopened.config.form).toBe(1);

await wrapper.setData({ sourceOptions: 'Variable' });
wrapper.vm.displayOptionChange();
expect(selectedControl.config.editable).toBe(false);
wrapper.destroy();
});
});
Loading