diff --git a/docs/entity/modifyFieldsOnValue.md b/docs/entity/modifyFieldsOnValue.md
index f379f1125..90c8abdb4 100644
--- a/docs/entity/modifyFieldsOnValue.md
+++ b/docs/entity/modifyFieldsOnValue.md
@@ -4,11 +4,11 @@ This feature allows to specify conditions to modify other fields based on curren
### Modification Object Properties
-| Property | Type | Description |
-| --------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
-| fieldValue\* | string | Value that will trigger the update, put `[[any_other_value]]` to trigger update for any other values than specified |
-| mode | string | Mode that adds possibility to use modification only on certain mode |
-| fieldsToModify | array | List of fields modifications that will be applied after com ponent value will match |
+| Property | Type | Description |
+| --------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- |
+| fieldValue\* | string | Value of current field that will trigger the update. Put `[[any_other_value]]` to make update for any other value than specified. |
+| mode | string | Mode that adds possibility to use modification only on certain mode. One of ( `create` / `edit` / `clone` / `config` ) |
+| fieldsToModify | array | List of fields modifications that will be applied after com ponent value will match. |
### fieldsToModify Properties
diff --git a/splunk_add_on_ucc_framework/global_config_validator.py b/splunk_add_on_ucc_framework/global_config_validator.py
index e50dc987a..b6ca1d87a 100644
--- a/splunk_add_on_ucc_framework/global_config_validator.py
+++ b/splunk_add_on_ucc_framework/global_config_validator.py
@@ -530,7 +530,7 @@ def _validate_groups(self) -> None:
f"Service {service['name']} uses group field {group_field} which is not defined in entity"
)
- def _is_circular(
+ def _is_circular_modification(
self,
mods: List[Any],
visited: Dict[str, str],
@@ -551,61 +551,73 @@ def _is_circular(
# no more dependent modification fields
visited[current_field] = DEAD_END
return visited
- else:
- for influenced_field in current_field_mods["influenced_fields"]:
- if influenced_field not in all_entity_fields:
- raise GlobalConfigValidatorException(
- f"""Modification in field '{current_field}' for not existing field '{influenced_field}'"""
- )
- if influenced_field == current_field:
- raise GlobalConfigValidatorException(
- f"""Field '{current_field}' tries to modify itself"""
- )
+
+ if current_field in current_field_mods["influenced_fields_value_change"]:
+ # field can modify itself except "value" property
+ raise GlobalConfigValidatorException(
+ f"""Field '{current_field}' tries to modify itself value"""
+ )
+
+ for influenced_field in current_field_mods["influenced_fields"]:
+ if influenced_field not in all_entity_fields:
+ raise GlobalConfigValidatorException(
+ f"""Modification in field '{current_field}' for not existing field '{influenced_field}'"""
+ )
+
+ if influenced_field in current_field_mods["influenced_fields_value_change"]:
if visited[influenced_field] == VISITING:
raise GlobalConfigValidatorException(
f"""Circular modifications for field '{influenced_field}' in field '{current_field}'"""
)
- else:
- visited = self._is_circular(
- mods, visited, all_entity_fields, influenced_field
- )
+ # check next influenced by value change field
+ visited = self._is_circular_modification(
+ mods, visited, all_entity_fields, influenced_field
+ )
+
# All dependent modifications fields are dead_end
visited[current_field] = DEAD_END
return visited
- def _check_if_circular(
+ def _check_if_circular_modification(
self,
all_entity_fields: List[Any],
fields_with_mods: List[Any],
modifications: List[Any],
) -> None:
visited = {field: "not_visited" for field in all_entity_fields}
-
for start_field in fields_with_mods:
# DFS algorithm for all fields with modifications
- visited = self._is_circular(
+ visited = self._is_circular_modification(
modifications, visited, all_entity_fields, start_field
)
@staticmethod
def _get_mods_data_for_single_entity(
- fields_with_mods: List[Any],
- all_modifications: List[Any],
entity: Dict[str, Any],
) -> List[Any]:
"""
- Add modification entity data to lists and returns them
+ Get modification entity data as lists
"""
+ entity_modifications = []
if "modifyFieldsOnValue" in entity:
+ influenced_fields_value_change = set()
influenced_fields = set()
- fields_with_mods.append(entity["field"])
for mods in entity["modifyFieldsOnValue"]:
for mod in mods["fieldsToModify"]:
influenced_fields.add(mod["fieldId"])
- all_modifications.append(
- {"fieldId": entity["field"], "influenced_fields": influenced_fields}
+
+ if (
+ mod.get("value") is not None
+ ): # circular deps are not a problem if not about value
+ influenced_fields_value_change.add(mod["fieldId"])
+ entity_modifications.append(
+ {
+ "fieldId": entity["field"],
+ "influenced_fields": influenced_fields,
+ "influenced_fields_value_change": influenced_fields_value_change,
+ }
)
- return [fields_with_mods, all_modifications]
+ return entity_modifications
@staticmethod
def _get_all_entities(
@@ -638,11 +650,13 @@ def _get_all_modification_data(
entities = self._get_all_entities(collections)
for entity in entities:
- self._get_mods_data_for_single_entity(
- fields_with_mods, all_modifications, entity
- )
all_fields.append(entity["field"])
+ if "modifyFieldsOnValue" in entity:
+ fields_with_mods.append(entity["field"])
+ entity_mods = self._get_mods_data_for_single_entity(entity)
+ all_modifications.extend(entity_mods)
+
return [fields_with_mods, all_modifications, all_fields]
def _validate_field_modifications(self) -> None:
@@ -664,7 +678,7 @@ def _validate_field_modifications(self) -> None:
all_fields_config,
) = self._get_all_modification_data(tabs)
- self._check_if_circular(
+ self._check_if_circular_modification(
all_fields_config, fields_with_mods_config, all_modifications_config
)
@@ -678,7 +692,7 @@ def _validate_field_modifications(self) -> None:
all_fields_inputs,
) = self._get_all_modification_data(services)
- self._check_if_circular(
+ self._check_if_circular_modification(
all_fields_inputs, fields_with_mods_inputs, all_modifications_inputs
)
diff --git a/tests/unit/test_global_config_validator.py b/tests/unit/test_global_config_validator.py
index 43dc3b9f2..185dc069e 100644
--- a/tests/unit/test_global_config_validator.py
+++ b/tests/unit/test_global_config_validator.py
@@ -337,7 +337,7 @@ def test_config_validation_modifications_on_change():
[
(
"invalid_config_with_modification_for_field_itself.json",
- "Field 'text1' tries to modify itself",
+ "Field 'text1' tries to modify itself value",
),
(
"invalid_config_with_modification_for_unexisiting_fields.json",
diff --git a/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json b/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
index 3c8e0406f..e662dea54 100644
--- a/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
+++ b/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
@@ -53,7 +53,7 @@
"fieldsToModify": [
{
"fieldId": "text2",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -62,7 +62,7 @@
"fieldsToModify": [
{
"fieldId": "text3",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -79,7 +79,7 @@
"fieldsToModify": [
{
"fieldId": "text3",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -88,7 +88,7 @@
"fieldsToModify": [
{
"fieldId": "text4",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -111,7 +111,7 @@
"fieldsToModify": [
{
"fieldId": "text5",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -120,7 +120,7 @@
"fieldsToModify": [
{
"fieldId": "text5",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -137,7 +137,7 @@
"fieldsToModify": [
{
"fieldId": "text6",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -146,7 +146,7 @@
"fieldsToModify": [
{
"fieldId": "text6",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -163,7 +163,7 @@
"fieldsToModify": [
{
"fieldId": "text7",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -172,7 +172,7 @@
"fieldsToModify": [
{
"fieldId": "text7",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -189,7 +189,7 @@
"fieldsToModify": [
{
"fieldId": "text1",
- "disabled": false
+ "value": "modification"
}
]
}
diff --git a/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json b/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
index eb7145f94..a87aabccf 100644
--- a/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
+++ b/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
@@ -53,7 +53,7 @@
"fieldsToModify": [
{
"fieldId": "text1",
- "disabled": false
+ "value": "value modification"
}
]
}
diff --git a/tests/unit/testdata/valid_config_with_modification_on_value_change.json b/tests/unit/testdata/valid_config_with_modification_on_value_change.json
index f39158c62..13b803d2c 100644
--- a/tests/unit/testdata/valid_config_with_modification_on_value_change.json
+++ b/tests/unit/testdata/valid_config_with_modification_on_value_change.json
@@ -54,6 +54,10 @@
{
"fieldId": "text2",
"disabled": false
+ },
+ {
+ "fieldId": "text1",
+ "label": "change label for itself"
}
]
},
diff --git a/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx b/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
index 43b7d1c5e..b49967885 100644
--- a/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
+++ b/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
-import { fn } from '@storybook/test';
+import { fn, userEvent, within } from '@storybook/test';
+
import BaseFormView from '../BaseFormView';
import {
PAGE_CONFIG_BOTH_OAUTH,
@@ -17,6 +18,8 @@ import {
getGlobalConfigMockGroupsForConfigPage,
getGlobalConfigMockModificationToGroupsConfig,
} from '../BaseFormConfigMock';
+import { getGlobalConfigMockModificationToFieldItself } from '../tests/configMocks';
+import { invariant } from '../../../util/invariant';
interface BaseFormStoriesProps extends BaseFormProps {
config: GlobalConfig;
@@ -143,3 +146,31 @@ export const GroupModificationsConfig: Story = {
platform: 'cloud',
},
};
+
+export const FieldModifyItself: Story = {
+ args: {
+ currentServiceState: {},
+ serviceName: 'account',
+ mode: 'create' as Mode,
+ page: 'configuration',
+ stanzaName: 'unknownStanza',
+ handleFormSubmit: fn(),
+ config: getGlobalConfigMockModificationToFieldItself(),
+ },
+};
+
+export const FieldModifyItselfAfterMods: Story = {
+ args: FieldModifyItself.args,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const modifyInputText = canvas
+ .getAllByRole('textbox')
+ .find((el) => el.getAttribute('value') === 'default value');
+
+ invariant(modifyInputText, 'modification input field should be defined');
+
+ await userEvent.clear(modifyInputText);
+ await userEvent.type(modifyInputText, 'modify itself');
+ },
+};
diff --git a/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png
new file mode 100644
index 000000000..3b7febda1
--- /dev/null
+++ b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7bb703699c8c301f552a38459fb9b237d1f7a4281ce011d3e3fa1ce4592917c0
+size 22951
diff --git a/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png
new file mode 100644
index 000000000..1a20c14c2
--- /dev/null
+++ b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8f806fad0fff2f0417063c3d1638107f87f3edbcc373218c37ee413df522109
+size 19293
diff --git a/ui/src/components/BaseFormView/BaseFormView.test.tsx b/ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
similarity index 92%
rename from ui/src/components/BaseFormView/BaseFormView.test.tsx
rename to ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
index 533c388aa..63b31af09 100644
--- a/ui/src/components/BaseFormView/BaseFormView.test.tsx
+++ b/ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
@@ -2,16 +2,16 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
-import { getGlobalConfigMock } from '../../mocks/globalConfigMock';
-import { setUnifiedConfig } from '../../util/util';
-import BaseFormView from './BaseFormView';
-import { getBuildDirPath } from '../../util/script';
-import mockCustomControlMockForTest from '../CustomControl/CustomControlMockForTest';
+import { getGlobalConfigMock } from '../../../mocks/globalConfigMock';
+import { getBuildDirPath } from '../../../util/script';
+import { setUnifiedConfig } from '../../../util/util';
import {
getGlobalConfigMockCustomControl,
getGlobalConfigMockGroupsForInputPage,
getGlobalConfigMockGroupsForConfigPage,
-} from './BaseFormConfigMock';
+} from '../BaseFormConfigMock';
+import mockCustomControlMockForTest from '../../CustomControl/CustomControlMockForTest';
+import BaseFormView from '../BaseFormView';
const handleFormSubmit = jest.fn();
diff --git a/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx b/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx
new file mode 100644
index 000000000..bdbcb4800
--- /dev/null
+++ b/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx
@@ -0,0 +1,62 @@
+import { render, screen, within } from '@testing-library/react';
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+
+import { setUnifiedConfig } from '../../../util/util';
+import BaseFormView from '../BaseFormView';
+import { getGlobalConfigMockModificationToFieldItself } from './configMocks';
+import { invariant } from '../../../util/invariant';
+
+const handleFormSubmit = jest.fn();
+
+const PAGE_CONF = 'configuration';
+const SERVICE_NAME = 'account';
+const STANZA_NAME = 'stanzaName';
+
+it('should modify correctly all properties of field, self modification', async () => {
+ const mockConfig = getGlobalConfigMockModificationToFieldItself();
+ setUnifiedConfig(mockConfig);
+
+ render(
+
+ );
+
+ await screen.findByText('default label');
+
+ const modifyTextField = document.querySelector(
+ '[data-name="text_field_with_modifications"]'
+ ) as HTMLElement;
+
+ expect(modifyTextField).toBeInTheDocument();
+
+ invariant(modifyTextField, 'modification field should be defined');
+
+ expect(within(modifyTextField).getByTestId('help')).toHaveTextContent('default help');
+ expect(within(modifyTextField).getByTestId('label')).toHaveTextContent('default label');
+ expect(within(modifyTextField).getByTestId('msg-markdown')).toHaveTextContent(
+ 'default markdown message'
+ );
+ expect(within(modifyTextField).queryByText('*')).not.toBeInTheDocument();
+
+ const inputComponent = within(modifyTextField).getByRole('textbox');
+ await userEvent.clear(inputComponent);
+ await userEvent.type(inputComponent, 'modify itself');
+
+ expect(within(modifyTextField).getByTestId('help')).toHaveTextContent(
+ 'help after modification'
+ );
+ expect(within(modifyTextField).getByTestId('label')).toHaveTextContent(
+ 'label after modification'
+ );
+ expect(within(modifyTextField).getByTestId('msg-markdown')).toHaveTextContent(
+ 'markdown message after modification'
+ );
+ expect(within(modifyTextField).queryByText('*')).toBeInTheDocument();
+});
diff --git a/ui/src/components/BaseFormView/tests/configMocks.ts b/ui/src/components/BaseFormView/tests/configMocks.ts
new file mode 100644
index 000000000..c6c338e99
--- /dev/null
+++ b/ui/src/components/BaseFormView/tests/configMocks.ts
@@ -0,0 +1,169 @@
+import { z } from 'zod';
+import { GlobalConfigSchema } from '../../../types/globalConfig/globalConfig';
+
+const CONFIG_MOCK_MODIFICATION_ON_VALUE_CHANGE_CONFIG = {
+ pages: {
+ configuration: {
+ tabs: [
+ {
+ name: 'account',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Account Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ {
+ type: 'string',
+ errorMsg: 'Length of input name should be between 1 and 100',
+ minLength: 1,
+ maxLength: 100,
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the account.',
+ required: true,
+ },
+ {
+ type: 'text',
+ label: 'Example text field',
+ field: 'text_field_with_modifications',
+ help: 'Example text field with modification',
+ required: false,
+ defaultValue: 'default value',
+ modifyFieldsOnValue: [
+ {
+ fieldValue: 'default value',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_with_modifications',
+ disabled: false,
+ required: false,
+ help: 'default help',
+ label: 'default label',
+ markdownMessage: {
+ text: 'default markdown message',
+ },
+ },
+ ],
+ },
+ {
+ fieldValue: 'modify itself',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_with_modifications',
+ disabled: false,
+ required: true,
+ help: 'help after modification',
+ label: 'label after modification',
+ markdownMessage: {
+ text: 'markdown message after modification',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'text',
+ label: 'Example text field to be modified',
+ field: 'text_field_to_be_modified',
+ help: 'Example text field to be modified',
+ required: false,
+ modifyFieldsOnValue: [
+ {
+ fieldValue: '[[any_other_value]]',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_to_be_modified',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ title: 'Accounts',
+ },
+ ],
+ title: 'Configuration',
+ description: 'Set up your add-on',
+ },
+ inputs: {
+ services: [
+ {
+ name: 'demo_input',
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ {
+ type: 'string',
+ errorMsg: 'Length of input name should be between 1 and 100',
+ minLength: 1,
+ maxLength: 100,
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the data input.',
+ required: true,
+ encrypted: false,
+ },
+ ],
+ title: 'demo_input',
+ },
+ ],
+ title: 'Inputs',
+ description: 'Manage your data inputs',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ moreInfo: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ },
+ },
+ meta: {
+ name: 'demo_addon_for_splunk',
+ restRoot: 'demo_addon_for_splunk',
+ version: '5.31.1R85f0e18e',
+ displayName: 'Demo Add-on for Splunk',
+ schemaVersion: '0.0.3',
+ checkForUpdates: false,
+ searchViewDefault: false,
+ },
+} satisfies z.input;
+
+export function getGlobalConfigMockModificationToFieldItself() {
+ return GlobalConfigSchema.parse(CONFIG_MOCK_MODIFICATION_ON_VALUE_CHANGE_CONFIG);
+}