Deletes CPA values on CPA field type change (#31122)

* Deletes CPA values on CPA field type change

* Fix error method name reference

* Cleans the state when a CPA field's type is updated

* Fix types

* Fix linter

* Webapp no longer makes a decision on the change and server sents a flag in the WS message

* Fix linter

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Этот коммит содержится в:
Miguel de la Cruz
2025-05-27 13:38:05 +02:00
коммит произвёл GitHub
родитель b3649132d0
Коммит e51ea025db
15 изменённых файлов: 684 добавлений и 61 удалений

Просмотреть файл

@@ -1952,9 +1952,23 @@ export function handleCustomAttributesCreated(msg) {
}
export function handleCustomAttributesUpdated(msg) {
return {
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED,
data: msg.data.field,
return (dispatch) => {
const {field, delete_values: deleteValues} = msg.data;
// Check if server indicates values should be deleted
if (deleteValues) {
// Clear values for the field when type changes
dispatch({
type: UserTypes.CLEAR_CPA_VALUES,
data: {fieldId: field.id},
});
}
// Update the field
dispatch({
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED,
data: field,
});
};
}

Просмотреть файл

@@ -1461,4 +1461,181 @@ describe('handleCustomAttributeCRUD', () => {
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field2.id)[0]).toBeTruthy();
});
describe('handleCustomAttributesUpdated', () => {
test('should update the CustomAttributeField in the state', () => {
const testStore = realConfigureStore(makeInitialState());
// First create a field
testStore.dispatch(handleCustomAttributesCreated({
event: SocketEvents.CPA_FIELD_CREATED,
data: {
field: field1,
},
}));
let cpaFields = getCustomProfileAttributes(testStore.getState());
expect(cpaFields).toBeTruthy();
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field1.id)[0].name).toEqual(field1.name);
// Update the field
const updatedField = {...field1, name: 'Updated Field Name'};
testStore.dispatch(handleCustomAttributesUpdated({
event: SocketEvents.CPA_FIELD_UPDATED,
data: {
field: updatedField,
},
}));
cpaFields = getCustomProfileAttributes(testStore.getState());
expect(cpaFields).toBeTruthy();
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field1.id)[0].name).toEqual('Updated Field Name');
});
test('should clear values when delete_values is true', () => {
// Set up a state with a user that has a custom profile attribute value
const testStore = realConfigureStore({
entities: {
general: {},
users: {
profiles: {
user1: {
id: 'user1',
custom_profile_attributes: {
[field1.id]: 'some value',
},
},
},
},
},
});
// First create a field
testStore.dispatch(handleCustomAttributesCreated({
event: SocketEvents.CPA_FIELD_CREATED,
data: {
field: field1,
},
}));
// Update the field with delete_values flag
const updatedField = {...field1, type: 'select'};
testStore.dispatch(handleCustomAttributesUpdated({
event: SocketEvents.CPA_FIELD_UPDATED,
data: {
field: updatedField,
delete_values: true,
},
}));
// Check that the field was updated
const cpaFields = getCustomProfileAttributes(testStore.getState());
expect(cpaFields).toBeTruthy();
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field1.id)[0].type).toEqual('select');
// Check that the user's values for this field were cleared
const user = testStore.getState().entities.users.profiles.user1;
expect(user.custom_profile_attributes?.[field1.id]).toBeFalsy();
});
test('should not clear values when delete_values is false', () => {
// Set up a state with a user that has a custom profile attribute value
const testStore = realConfigureStore({
entities: {
general: {},
users: {
profiles: {
user1: {
id: 'user1',
custom_profile_attributes: {
[field1.id]: 'some value',
},
},
},
},
},
});
// First create a field
testStore.dispatch(handleCustomAttributesCreated({
event: SocketEvents.CPA_FIELD_CREATED,
data: {
field: field1,
},
}));
// Update the field but with delete_values flag set to false
const updatedField = {...field1, name: 'Updated Field Name', type: 'text'};
testStore.dispatch(handleCustomAttributesUpdated({
event: SocketEvents.CPA_FIELD_UPDATED,
data: {
field: updatedField,
delete_values: false,
},
}));
// Check that the field was updated
const cpaFields = getCustomProfileAttributes(testStore.getState());
expect(cpaFields).toBeTruthy();
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field1.id)[0].name).toEqual('Updated Field Name');
// Check that the user's values for this field were NOT cleared
const user = testStore.getState().entities.users.profiles.user1;
expect(user.custom_profile_attributes).toBeTruthy();
expect(user.custom_profile_attributes[field1.id]).toEqual('some value');
});
test('should not clear values when delete_values is not specified', () => {
// Set up a state with a user that has a custom profile attribute value
const testStore = realConfigureStore({
entities: {
general: {},
users: {
profiles: {
user1: {
id: 'user1',
custom_profile_attributes: {
[field1.id]: 'some value',
},
},
},
},
},
});
// First create a field
testStore.dispatch(handleCustomAttributesCreated({
event: SocketEvents.CPA_FIELD_CREATED,
data: {
field: field1,
},
}));
// Update the field without specifying delete_values
const updatedField = {...field1, type: 'number'};
testStore.dispatch(handleCustomAttributesUpdated({
event: SocketEvents.CPA_FIELD_UPDATED,
data: {
field: updatedField,
// delete_values not specified
},
}));
// Check that the field was updated
const cpaFields = getCustomProfileAttributes(testStore.getState());
expect(cpaFields).toBeTruthy();
expect(cpaFields.length).toEqual(1);
expect(cpaFields.filter(({id}) => id === field1.id)[0].type).toEqual('number');
// Check that the user's values for this field were NOT cleared
const user = testStore.getState().entities.users.profiles.user1;
expect(user.custom_profile_attributes).toBeTruthy();
expect(user.custom_profile_attributes[field1.id]).toEqual('some value');
});
});
});

Просмотреть файл

@@ -74,4 +74,5 @@ export default keyMirror({
RECEIVED_FILTERED_USER_STATS: null,
PROFILE_NO_LONGER_VISIBLE: null,
LOGIN: null,
CLEAR_CPA_VALUES: null,
});

Просмотреть файл

@@ -1086,6 +1086,103 @@ describe('Reducers.users', () => {
expect(updatedProfiles.first_user_id.custom_profile_attributes!.field1).toEqual('updatedValue');
});
test('UserTypes.CLEAR_CPA_VALUES, should clear the custom profile attribute value for specified field ID', () => {
const user1 = TestHelper.getUserMock({
id: 'user1',
custom_profile_attributes: {
field1: 'value1',
field2: 'value2',
},
});
const user2 = TestHelper.getUserMock({
id: 'user2',
custom_profile_attributes: {
field1: 'user2_value1',
field3: 'user2_value3',
},
});
const user3 = TestHelper.getUserMock({
id: 'user3',
// No custom profile attributes
});
const state = deepFreezeAndThrowOnMutation({
profiles: {user1, user2, user3},
}) as UsersState;
const action = {
type: UserTypes.CLEAR_CPA_VALUES,
data: {
fieldId: 'field1',
},
};
const newState = reducer(state, action);
// Check user1 (had field1, should be removed)
expect(newState.profiles.user1.custom_profile_attributes).toEqual({
field2: 'value2',
});
// Check user2 (had field1, should be removed)
expect(newState.profiles.user2.custom_profile_attributes).toEqual({
field3: 'user2_value3',
});
// Check user3 (had no attributes, should remain the same)
expect(newState.profiles.user3.custom_profile_attributes).toBeUndefined();
});
test('UserTypes.CLEAR_CPA_VALUES, should return the same state if no profiles have the specified field', () => {
const user1 = TestHelper.getUserMock({
id: 'user1',
custom_profile_attributes: {
field1: 'value1',
field2: 'value2',
},
});
const state = deepFreezeAndThrowOnMutation({
profiles: {user1},
}) as UsersState;
const action = {
type: UserTypes.CLEAR_CPA_VALUES,
data: {
fieldId: 'non_existent_field',
},
};
const newState = reducer(state, action);
// State should have changed (new object), but values should remain the same
expect(newState).not.toBe(state);
expect(newState.profiles.user1).toBe(user1);
expect(newState.profiles.user1.custom_profile_attributes).toEqual({
field1: 'value1',
field2: 'value2',
});
});
test('UserTypes.CLEAR_CPA_VALUES, should handle empty profiles state', () => {
const state = deepFreezeAndThrowOnMutation({
profiles: {},
}) as UsersState;
const action = {
type: UserTypes.CLEAR_CPA_VALUES,
data: {
fieldId: 'field1',
},
};
const newState = reducer(state as UsersState, action);
expect(newState.profiles).toEqual({});
});
});
test('PROFILE_NO_LONGER_VISIBLE should remove references to users from state', () => {

Просмотреть файл

@@ -236,6 +236,26 @@ function profiles(state: UsersState['profiles'] = {}, action: MMReduxAction) {
const profileAttributes = {...existingProfile.custom_profile_attributes, ...customAttributeValues};
return receiveUserProfile(state, {...existingProfile, custom_profile_attributes: profileAttributes});
}
case UserTypes.CLEAR_CPA_VALUES: {
const {fieldId} = action.data;
return Object.values(state).reduce<Record<string, UserProfile>>((nextState, profile) => {
// Only modify profiles that have this field value
if (profile.custom_profile_attributes && profile.custom_profile_attributes[fieldId] !== undefined) {
const newAttributes = {...profile.custom_profile_attributes};
delete newAttributes[fieldId];
nextState[profile.id] = {
...profile,
custom_profile_attributes: newAttributes,
};
} else {
nextState[profile.id] = profile;
}
return nextState;
}, {});
}
case UserTypes.RECEIVED_PROFILES_LIST: {
const users: UserProfile[] = action.data;