Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ collections: # A list of collections the CMS should be able to edit
multiple: true,
}
- { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
- { label: 'Conditional string', name: 'conditional_string', widget: 'string', condition: { field: 'select', value: 'a', operator: '!=' } }
- label: 'Conditional object'
name: 'conditional_object'
widget: 'object'
condition: { field: 'boolean', value: true }
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Color', name: 'color', widget: 'color' }
- label: 'Object'
name: 'object'
Expand Down
7 changes: 7 additions & 0 deletions packages/decap-cms-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ declare module 'decap-cms-core' {
default_locale?: string;
}

interface Condition {
field: string;
value: string | boolean | number;
operator?: '==' | '!=' | '>' | '<' | '>=' | '<=';
}

export interface CmsFieldBase {
name: string;
label?: string;
Expand All @@ -61,6 +67,7 @@ declare module 'decap-cms-core' {
media_folder?: string;
public_folder?: string;
comment?: string;
condition?: Condition;
}

export interface CmsFieldBoolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ function getFieldValue({ field, entry, isTranslatable, locale }) {
return entry.getIn(['data', field.get('name')]);
}

function calculateCondition({field, fields, entry, locale, isTranslatable}) {
const condition = field.get('condition')
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

[nitpick] Missing semicolon at the end of the statement. This is inconsistent with the code style used elsewhere in the function (lines 101-103).

Suggested change
const condition = field.get('condition')
const condition = field.get('condition');

Copilot uses AI. Check for mistakes.
if (!condition) return true;

const condFieldName = condition.get('field');
const operator = condition.get('operator') || '==';
const condValue = condition.get('value');

const condField = fields.find(f => f.get('name') === condFieldName);
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The calculateCondition function doesn't handle the case where the referenced field is not found. If condField is undefined (when no field matches condFieldName), calling getFieldValue will fail. Add a null check:

const condField = fields.find(f => f.get('name') === condFieldName);
if (!condField) return false; // or true, depending on desired behavior
Suggested change
const condField = fields.find(f => f.get('name') === condFieldName);
const condField = fields.find(f => f.get('name') === condFieldName);
if (!condField) return false;

Copilot uses AI. Check for mistakes.
let condFieldValue = getFieldValue({ field: condField, entry, locale, isTranslatable, });
condFieldValue = condFieldValue?.toJS ? condFieldValue.toJS() : condFieldValue;

switch (operator) {
case '==':
return condFieldValue == condValue;
case '!=':
return condFieldValue != condValue;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The comparison operators use loose equality (== and !=) instead of strict equality (=== and !==). This can lead to unexpected type coercion. For example, "0" == 0 would be true with loose equality. Consider using strict equality operators for more predictable behavior.

Copilot uses AI. Check for mistakes.
case '<':
return condFieldValue < condValue;
case '>':
return condFieldValue > condValue;
case '<=':
return condFieldValue <= condValue;
case '>=':
return condFieldValue >= condValue;
default:
return condFieldValue == condValue;
}
}

export default class ControlPane extends React.Component {
state = {
selectedLocale: this.props.locale,
Expand All @@ -116,49 +146,56 @@ export default class ControlPane extends React.Component {
this.props.onLocaleChange(val);
};

copyFromOtherLocale =
({ targetLocale, t }) =>
sourceLocale => {
if (
!window.confirm(
t('editor.editorControlPane.i18n.copyFromLocaleConfirm', {
locale: sourceLocale.toUpperCase(),
}),
)
) {
return;
}
const { entry, collection } = this.props;
const { locales, defaultLocale } = getI18nInfo(collection);

const locale = this.state.selectedLocale;
const i18n = locales && {
currentLocale: locale,
locales,
defaultLocale,
};

this.props.fields.forEach(field => {
if (isFieldTranslatable(field, targetLocale, sourceLocale)) {
const copyValue = getFieldValue({
field,
entry,
locale: sourceLocale,
isTranslatable: sourceLocale !== defaultLocale,
});
if (copyValue) this.props.onChange(field, copyValue, undefined, i18n);
}
});
copyFromOtherLocale = ({ targetLocale, t }) => sourceLocale => {
if (
!window.confirm(
t('editor.editorControlPane.i18n.copyFromLocaleConfirm', {
locale: sourceLocale.toUpperCase(),
}),
)
) {
return;
}
const { entry, collection } = this.props;
const { locales, defaultLocale } = getI18nInfo(collection);

const locale = this.state.selectedLocale;
const i18n = locales && {
currentLocale: locale,
locales,
defaultLocale,
};

validate = async () => {
this.props.fields.forEach(field => {
if (field.get('widget') === 'hidden') return;
if (isFieldTranslatable(field, targetLocale, sourceLocale)) {
const copyValue = getFieldValue({
field,
entry,
locale: sourceLocale,
isTranslatable: sourceLocale !== defaultLocale,
});
if (copyValue) this.props.onChange(field, copyValue, undefined, i18n);
}
});
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The copyFromOtherLocale function doesn't check if fields meet their condition before copying values. This could result in copying values to fields that are currently hidden due to unmet conditions. Consider adding a condition check similar to the one in the validate method and the render logic.

Copilot uses AI. Check for mistakes.
};

validate = async () => {
const { fields, entry, collection } = this.props;

fields.forEach(field => {
const isConditionMet = calculateCondition({
field,
fields,
entry,
locale: this.state.selectedLocale,
isTranslatable: hasI18n(collection),
});

if (field.get('widget') === 'hidden' || !isConditionMet) return;

const control = this.childRefs[field.get('name')];
const validateFn = control?.innerWrappedControl?.validate ?? control?.validate;
if (validateFn) {
validateFn();
}
if (validateFn) validateFn();
});
};

Expand Down Expand Up @@ -224,6 +261,14 @@ export default class ControlPane extends React.Component {
const isDuplicate = isFieldDuplicate(field, locale, defaultLocale);
const isHidden = isFieldHidden(field, locale, defaultLocale);
const key = i18n ? `${locale}_${i}` : i;
const isConditionMet = calculateCondition({
field,
fields,
entry,
locale,
isTranslatable,
});
if (!isConditionMet) return
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

Missing return value. When the condition is not met, this function returns undefined implicitly. For clarity and consistency with React rendering, explicitly return null:

if (!isConditionMet) return null;
Suggested change
if (!isConditionMet) return
if (!isConditionMet) return null;

Copilot uses AI. Check for mistakes.

return (
<EditorControl
Expand Down
8 changes: 8 additions & 0 deletions packages/decap-cms-core/src/constants/configSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ function fieldsConfig() {
field: { $ref: `field_${id}` },
fields: { $ref: `fields_${id}` },
types: { $ref: `fields_${id}` },
condition: {
type: 'object',
properties: {
field: { type: 'string' },
value: { types: ['string', 'boolean'] },
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The schema uses types (plural) instead of type (singular) for the value property. This should be type: ['string', 'boolean', 'number'] to match JSON Schema standards and be consistent with how other properties are defined in the schema.

Suggested change
value: { types: ['string', 'boolean'] },
value: { type: ['string', 'boolean'] },

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The schema definition for the value property is missing the number type. According to the TypeScript definition in redux.ts and index.d.ts, the value should accept string | boolean | number. Add 'number' to the types array: type: ['string', 'boolean', 'number'].

Suggested change
value: { types: ['string', 'boolean'] },
value: { type: ['string', 'boolean', 'number'] },

Copilot uses AI. Check for mistakes.
operator: { type: 'string', enum: ['==', '!=', '>', '<', '>=', '<='] },
},
},
},
select: { $data: '0/widget' },
selectCases: {
Expand Down
7 changes: 7 additions & 0 deletions packages/decap-cms-core/src/types/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export interface CmsI18nConfig {
default_locale?: string;
}

interface Condition {
field: string;
value: string | boolean | number;
operator?: '==' | '!=' | '>' | '<' | '>=' | '<=';
}

export interface CmsFieldBase {
name: string;
label?: string;
Expand All @@ -77,6 +83,7 @@ export interface CmsFieldBase {
media_folder?: string;
public_folder?: string;
comment?: string;
condition?: Condition;
}

export interface CmsFieldBoolean {
Expand Down
Loading