Skip to content

Commit 57abe58

Browse files
author
Bianka Drabova
committed
fix: prevent duplicate entries in list widget with relation field
1 parent 0c7d327 commit 57abe58

File tree

2 files changed

+159
-5
lines changed

2 files changed

+159
-5
lines changed

packages/decap-cms-widget-list/src/ListControl.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,30 @@ export default class ListControl extends React.Component {
427427
const withNameKey =
428428
this.getValueType() !== valueTypes.SINGLE ||
429429
(this.getValueType() === valueTypes.SINGLE && listFieldObjectWidget);
430+
431+
if (f.get('widget') === 'relation') {
432+
const isDuplicate =
433+
value &&
434+
value.some((item, idx) => {
435+
if (idx === index) return false;
436+
const itemValue = withNameKey ? item.get(f.get('name')) : item;
437+
return itemValue === newValue;
438+
});
439+
440+
if (isDuplicate) {
441+
console.warn(`⚠️ Duplicate: "${newValue}" already selected`);
442+
443+
this.duplicateErrorIndex = index;
444+
this.forceUpdate();
445+
446+
return;
447+
}
448+
}
449+
450+
if (this.duplicateErrorIndex === index) {
451+
this.duplicateErrorIndex = null;
452+
}
453+
430454
const newObjectValue = withNameKey
431455
? this.getObjectValue(index).set(f.get('name'), newValue)
432456
: newValue;
@@ -652,6 +676,9 @@ export default class ListControl extends React.Component {
652676
let field = this.props.field;
653677
const hasError = this.hasError(index);
654678
const isVariableTypesList = this.getValueType() === valueTypes.MIXED;
679+
680+
const hasDuplicateError = this.duplicateErrorIndex === index;
681+
655682
if (isVariableTypesList) {
656683
field = getTypedFieldForValue(field, item);
657684
if (!field) {
@@ -667,6 +694,22 @@ export default class ListControl extends React.Component {
667694
id={key}
668695
keys={keys}
669696
>
697+
{hasDuplicateError && (
698+
<div
699+
style={{
700+
background: '#fee',
701+
color: '#c33',
702+
padding: '12px',
703+
marginBottom: '12px',
704+
borderRadius: '4px',
705+
border: '1px solid #fcc',
706+
fontSize: '14px',
707+
fontWeight: 'bold',
708+
}}
709+
>
710+
❌ This entry is already selected in this list
711+
</div>
712+
)}
670713
{isVariableTypesList && (
671714
<LabelComponent
672715
field={field}
@@ -810,10 +853,8 @@ export default class ListControl extends React.Component {
810853
}
811854

812855
render() {
813-
if (this.getValueType() !== null) {
814-
return this.renderListControl();
815-
} else {
816-
return this.renderInput();
817-
}
856+
return (
857+
<div>{this.getValueType() !== null ? this.renderListControl() : this.renderInput()}</div>
858+
);
818859
}
819860
}

packages/decap-cms-widget-list/src/__tests__/ListControl.spec.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,4 +789,117 @@ describe('ListControl', () => {
789789
listControl.validate();
790790
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
791791
});
792+
793+
it('should prevent duplicate relation entries', () => {
794+
const field = fromJS({
795+
name: 'featured_posts',
796+
label: 'Featured Posts',
797+
widget: 'list',
798+
field: {
799+
name: 'featured_entries',
800+
widget: 'relation',
801+
collection: 'posts',
802+
},
803+
});
804+
805+
const { getByTestId } = render(
806+
<ListControl
807+
{...props}
808+
field={field}
809+
value={fromJS([
810+
{ featured_entries: 'post-1' },
811+
{ featured_entries: 'post-2' },
812+
])}
813+
/>,
814+
);
815+
816+
const listControl = new ListControl({
817+
...props,
818+
field,
819+
value: fromJS([
820+
{ featured_entries: 'post-1' },
821+
{ featured_entries: 'post-2' },
822+
]),
823+
});
824+
825+
const handleChange = listControl.handleChangeFor(0);
826+
const relationField = fromJS({
827+
widget: 'relation',
828+
name: 'featured_entries',
829+
});
830+
831+
handleChange(relationField, 'post-2');
832+
833+
expect(props.onChange).not.toHaveBeenCalled();
834+
835+
expect(listControl.duplicateErrorIndex).toBe(0);
836+
});
837+
838+
it('should allow non-duplicate relation entries', () => {
839+
const field = fromJS({
840+
name: 'featured_posts',
841+
label: 'Featured Posts',
842+
widget: 'list',
843+
field: {
844+
name: 'featured_entries',
845+
widget: 'relation',
846+
collection: 'posts',
847+
},
848+
});
849+
850+
const listControl = new ListControl({
851+
...props,
852+
field,
853+
value: fromJS([
854+
{ featured_entries: 'post-1' },
855+
{ featured_entries: 'post-2' },
856+
]),
857+
});
858+
859+
const handleChange = listControl.handleChangeFor(0);
860+
const relationField = fromJS({
861+
widget: 'relation',
862+
name: 'featured_entries',
863+
});
864+
865+
handleChange(relationField, 'post-new');
866+
867+
expect(props.onChange).toHaveBeenCalled();
868+
869+
expect(listControl.duplicateErrorIndex).toBeNull();
870+
});
871+
872+
it('should clear duplicate error when value changes', () => {
873+
const field = fromJS({
874+
name: 'featured_posts',
875+
label: 'Featured Posts',
876+
widget: 'list',
877+
field: {
878+
name: 'featured_entries',
879+
widget: 'relation',
880+
collection: 'posts',
881+
},
882+
});
883+
884+
const listControl = new ListControl({
885+
...props,
886+
field,
887+
value: fromJS([
888+
{ featured_entries: 'post-1' },
889+
{ featured_entries: 'post-2' },
890+
]),
891+
});
892+
893+
listControl.duplicateErrorIndex = 0;
894+
895+
const handleChange = listControl.handleChangeFor(0);
896+
const relationField = fromJS({
897+
widget: 'relation',
898+
name: 'featured_entries',
899+
});
900+
901+
handleChange(relationField, 'post-new');
902+
903+
expect(listControl.duplicateErrorIndex).toBeNull();
904+
});
792905
});

0 commit comments

Comments
 (0)