MM-62564 - implement websockets for CPA (#30169)
* implement websockets for CPA * fix for testing with server * revert feature flag * fix unit test * update constant names * add reconnect handler --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
806fce3030
Коммит
1803f1c215
@@ -37,7 +37,7 @@ import {
|
|||||||
} from 'mattermost-redux/actions/channels';
|
} from 'mattermost-redux/actions/channels';
|
||||||
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
||||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||||
import {setServerVersion, getClientConfig} from 'mattermost-redux/actions/general';
|
import {setServerVersion, getClientConfig, getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
||||||
import {getGroup as fetchGroup} from 'mattermost-redux/actions/groups';
|
import {getGroup as fetchGroup} from 'mattermost-redux/actions/groups';
|
||||||
import {
|
import {
|
||||||
getCustomEmojiForReaction,
|
getCustomEmojiForReaction,
|
||||||
@@ -285,6 +285,9 @@ export function reconnect() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Refresh custom profile attributes on reconnect
|
||||||
|
dispatch(getCustomProfileAttributeFields());
|
||||||
|
|
||||||
if (state.websocket.lastDisconnectAt) {
|
if (state.websocket.lastDisconnectAt) {
|
||||||
dispatch(checkForModifiedUsers());
|
dispatch(checkForModifiedUsers());
|
||||||
}
|
}
|
||||||
@@ -630,6 +633,18 @@ export function handleEvent(msg) {
|
|||||||
case SocketEvents.HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED:
|
case SocketEvents.HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED:
|
||||||
dispatch(handleHostedCustomerSignupProgressUpdated(msg));
|
dispatch(handleHostedCustomerSignupProgressUpdated(msg));
|
||||||
break;
|
break;
|
||||||
|
case SocketEvents.CPA_VALUES_UPDATED:
|
||||||
|
dispatch(handleCustomAttributeValuesUpdated(msg));
|
||||||
|
break;
|
||||||
|
case SocketEvents.CPA_FIELD_CREATED:
|
||||||
|
dispatch(handleCustomAttributesCreated(msg));
|
||||||
|
break;
|
||||||
|
case SocketEvents.CPA_FIELD_UPDATED:
|
||||||
|
dispatch(handleCustomAttributesUpdated(msg));
|
||||||
|
break;
|
||||||
|
case SocketEvents.CPA_FIELD_DELETED:
|
||||||
|
dispatch(handleCustomAttributesDeleted(msg));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1898,3 +1913,31 @@ function handleChannelBookmarkSorted(msg) {
|
|||||||
data: {channelId: msg.broadcast.channel_id, bookmarks},
|
data: {channelId: msg.broadcast.channel_id, bookmarks},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleCustomAttributeValuesUpdated(msg) {
|
||||||
|
return {
|
||||||
|
type: UserTypes.RECEIVED_CPA_VALUES,
|
||||||
|
data: {userID: msg.data.user_id, customAttributeValues: msg.data.values},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCustomAttributesCreated(msg) {
|
||||||
|
return {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED,
|
||||||
|
data: msg.data.field,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCustomAttributesUpdated(msg) {
|
||||||
|
return {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED,
|
||||||
|
data: msg.data.field,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCustomAttributesDeleted(msg) {
|
||||||
|
return {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED,
|
||||||
|
data: msg.data.field_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import {CloudTypes} from 'mattermost-redux/action_types';
|
import {CloudTypes} from 'mattermost-redux/action_types';
|
||||||
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
||||||
import {fetchAllMyTeamsChannels} from 'mattermost-redux/actions/channels';
|
import {fetchAllMyTeamsChannels} from 'mattermost-redux/actions/channels';
|
||||||
|
import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
||||||
import {getGroup} from 'mattermost-redux/actions/groups';
|
import {getGroup} from 'mattermost-redux/actions/groups';
|
||||||
import {
|
import {
|
||||||
getPostThreads,
|
getPostThreads,
|
||||||
@@ -11,7 +12,8 @@ import {
|
|||||||
} from 'mattermost-redux/actions/posts';
|
} from 'mattermost-redux/actions/posts';
|
||||||
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
import {batchFetchStatusesProfilesGroupsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
|
||||||
import {getUser} from 'mattermost-redux/actions/users';
|
import {getUser} from 'mattermost-redux/actions/users';
|
||||||
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getStatusForUserId, getUser as stateUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {handleNewPost} from 'actions/post_actions';
|
import {handleNewPost} from 'actions/post_actions';
|
||||||
import {syncPostsInChannel} from 'actions/views/channel';
|
import {syncPostsInChannel} from 'actions/views/channel';
|
||||||
@@ -41,6 +43,10 @@ import {
|
|||||||
handleCloudSubscriptionChanged,
|
handleCloudSubscriptionChanged,
|
||||||
handleGroupAddedMemberEvent,
|
handleGroupAddedMemberEvent,
|
||||||
handleStatusChangedEvent,
|
handleStatusChangedEvent,
|
||||||
|
handleCustomAttributeValuesUpdated,
|
||||||
|
handleCustomAttributesCreated,
|
||||||
|
handleCustomAttributesUpdated,
|
||||||
|
handleCustomAttributesDeleted,
|
||||||
} from './websocket_actions';
|
} from './websocket_actions';
|
||||||
|
|
||||||
jest.mock('mattermost-redux/actions/posts', () => ({
|
jest.mock('mattermost-redux/actions/posts', () => ({
|
||||||
@@ -59,6 +65,11 @@ jest.mock('mattermost-redux/actions/status_profile_polling', () => ({
|
|||||||
batchFetchStatusesProfilesGroupsFromPosts: jest.fn(() => ({type: ''})),
|
batchFetchStatusesProfilesGroupsFromPosts: jest.fn(() => ({type: ''})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/actions/general', () => ({
|
||||||
|
...jest.requireActual('mattermost-redux/actions/general'),
|
||||||
|
getCustomProfileAttributeFields: jest.fn(() => ({type: 'CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED'})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('mattermost-redux/actions/groups', () => ({
|
jest.mock('mattermost-redux/actions/groups', () => ({
|
||||||
...jest.requireActual('mattermost-redux/actions/groups'),
|
...jest.requireActual('mattermost-redux/actions/groups'),
|
||||||
getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})),
|
getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})),
|
||||||
@@ -664,6 +675,11 @@ describe('reconnect', () => {
|
|||||||
reconnect();
|
reconnect();
|
||||||
expect(fetchAllMyTeamsChannels).toHaveBeenCalled();
|
expect(fetchAllMyTeamsChannels).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reload custom profile attribute fields on reconnect', () => {
|
||||||
|
reconnect();
|
||||||
|
expect(getCustomProfileAttributeFields).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleChannelUpdatedEvent', () => {
|
describe('handleChannelUpdatedEvent', () => {
|
||||||
@@ -1282,3 +1298,141 @@ describe('handleStatusChangedEvent', () => {
|
|||||||
expect(getStatusForUserId(testStore.getState(), currentUserId)).toBe(UserStatuses.OFFLINE);
|
expect(getStatusForUserId(testStore.getState(), currentUserId)).toBe(UserStatuses.OFFLINE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleCustomAttributeValuesUpdated', () => {
|
||||||
|
const currentUserId = 'user1';
|
||||||
|
|
||||||
|
function makeInitialState() {
|
||||||
|
return {
|
||||||
|
entities: {
|
||||||
|
users: {
|
||||||
|
profiles: {
|
||||||
|
user1: {id: currentUserId},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should add the CustomAttributeValues to the user', () => {
|
||||||
|
const testStore = realConfigureStore(makeInitialState());
|
||||||
|
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId)).toEqual({id: currentUserId});
|
||||||
|
|
||||||
|
testStore.dispatch(handleCustomAttributeValuesUpdated({
|
||||||
|
event: SocketEvents.CPA_VALUES_UPDATED,
|
||||||
|
data: {
|
||||||
|
user_id: currentUserId,
|
||||||
|
values: {field1: 'value1', field2: 'value2'},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes).toBeTruthy();
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes.field1).toEqual('value1');
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes.field2).toEqual('value2');
|
||||||
|
|
||||||
|
// update one field, add new field
|
||||||
|
testStore.dispatch(handleCustomAttributeValuesUpdated({
|
||||||
|
event: SocketEvents.CPA_VALUES_UPDATED,
|
||||||
|
data: {
|
||||||
|
user_id: currentUserId,
|
||||||
|
values: {field1: 'valueChanged', field3: 'new field'},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes).toBeTruthy();
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes.field1).toEqual('valueChanged');
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes.field2).toEqual('value2');
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes.field3).toEqual('new field');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore the CustomAttributeValues if no user', () => {
|
||||||
|
const testStore = realConfigureStore(makeInitialState());
|
||||||
|
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId)).toEqual({id: currentUserId});
|
||||||
|
|
||||||
|
testStore.dispatch(handleCustomAttributeValuesUpdated({
|
||||||
|
event: SocketEvents.CPA_VALUES_UPDATED,
|
||||||
|
data: {
|
||||||
|
user_id: 'nonExistantUser',
|
||||||
|
values: {field1: 'value1', field2: 'value2'},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(stateUser(testStore.getState(), 'nonExistintUser')).toBeFalsy();
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId)).toBeTruthy();
|
||||||
|
expect(stateUser(testStore.getState(), currentUserId).custom_profile_attributes).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleCustomAttributeCRUD', () => {
|
||||||
|
const field1 = {id: 'field1', groupid: 'group1', name: 'FIELD ONE', type: 'text'};
|
||||||
|
const field2 = {id: 'field2', groupid: 'group1', name: 'FIELD TWO', type: 'text'};
|
||||||
|
|
||||||
|
function makeInitialState() {
|
||||||
|
return {
|
||||||
|
entities: {
|
||||||
|
general: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should add the CustomAttributeField to the state', () => {
|
||||||
|
const testStore = realConfigureStore(makeInitialState());
|
||||||
|
|
||||||
|
testStore.dispatch(handleCustomAttributesCreated({
|
||||||
|
event: SocketEvents.CPA_FIELD_CREATED,
|
||||||
|
data: {
|
||||||
|
field: field1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let cpaFields = getCustomProfileAttributes(testStore.getState());
|
||||||
|
expect(cpaFields).toBeTruthy();
|
||||||
|
expect(Object.keys(cpaFields).length).toEqual(1);
|
||||||
|
expect(cpaFields.field1.type).toEqual(field1.type);
|
||||||
|
expect(cpaFields.field1.name).toEqual(field1.name);
|
||||||
|
|
||||||
|
// create second field
|
||||||
|
testStore.dispatch(handleCustomAttributesCreated({
|
||||||
|
event: SocketEvents.CPA_FIELD_CREATED,
|
||||||
|
data: {
|
||||||
|
field: field2,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
cpaFields = getCustomProfileAttributes(testStore.getState());
|
||||||
|
expect(cpaFields).toBeTruthy();
|
||||||
|
expect(Object.keys(cpaFields).length).toEqual(2);
|
||||||
|
expect(cpaFields.field2.type).toEqual(field2.type);
|
||||||
|
expect(cpaFields.field2.name).toEqual(field2.name);
|
||||||
|
|
||||||
|
// update field
|
||||||
|
testStore.dispatch(handleCustomAttributesUpdated({
|
||||||
|
event: SocketEvents.CPA_FIELD_UPDATED,
|
||||||
|
data: {
|
||||||
|
field: {...field1, name: 'Updated Name'},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
cpaFields = getCustomProfileAttributes(testStore.getState());
|
||||||
|
expect(cpaFields).toBeTruthy();
|
||||||
|
expect(Object.keys(cpaFields).length).toEqual(2);
|
||||||
|
expect(cpaFields.field1.name).toEqual('Updated Name');
|
||||||
|
expect(cpaFields.field2.name).toEqual(field2.name);
|
||||||
|
|
||||||
|
// delete field
|
||||||
|
testStore.dispatch(handleCustomAttributesDeleted({
|
||||||
|
event: SocketEvents.CPA_FIELD_DELETED,
|
||||||
|
data: {
|
||||||
|
field_id: field1.id,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
cpaFields = getCustomProfileAttributes(testStore.getState());
|
||||||
|
expect(cpaFields).toBeTruthy();
|
||||||
|
expect(Object.keys(cpaFields).length).toEqual(1);
|
||||||
|
expect(cpaFields.field2).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect} from 'react';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
import {getCustomProfileAttributeValues} from 'mattermost-redux/actions/users';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
|
||||||
import {getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general';
|
import {getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
@@ -17,42 +17,43 @@ const ProfilePopoverCustomAttributes = ({
|
|||||||
userID,
|
userID,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [customAttributeValues, setCustomAttributeValues] = useState<Record<string, string>>({});
|
const userProfile = useSelector((state: GlobalState) => getUser(state, userID));
|
||||||
const customProfileAttributeFields = useSelector((state: GlobalState) => getCustomProfileAttributes(state));
|
const customProfileAttributeFields = useSelector((state: GlobalState) => getCustomProfileAttributes(state));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchValues = async () => {
|
if (!userProfile.custom_profile_attributes) {
|
||||||
const response = await Client4.getUserCustomProfileAttributesValues(userID);
|
dispatch(getCustomProfileAttributeValues(userID));
|
||||||
setCustomAttributeValues(response);
|
|
||||||
};
|
|
||||||
dispatch(getCustomProfileAttributeFields());
|
|
||||||
fetchValues();
|
|
||||||
}, [userID, dispatch]);
|
|
||||||
const attributeSections = Object.values(customProfileAttributeFields).map((attribute) => {
|
|
||||||
const value = customAttributeValues[attribute.id];
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={'customAttribute_' + attribute.id}
|
|
||||||
className='user-popover__custom_attributes'
|
|
||||||
>
|
|
||||||
<strong
|
|
||||||
id={`user-popover__custom_attributes-title-${attribute.id}`}
|
|
||||||
className='user-popover__subtitle'
|
|
||||||
>
|
|
||||||
{attribute.name}
|
|
||||||
</strong>
|
|
||||||
<p
|
|
||||||
aria-labelledby={`user-popover__custom_attributes-title-${attribute.id}`}
|
|
||||||
className='user-popover__subtitle-text'
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
const attributeSections = Object.values(customProfileAttributeFields).map((attribute) => {
|
||||||
|
if (userProfile.custom_profile_attributes) {
|
||||||
|
const value = userProfile.custom_profile_attributes[attribute.id];
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={'customAttribute_' + attribute.id}
|
||||||
|
className='user-popover__custom_attributes'
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
id={`user-popover__custom_attributes-title-${attribute.id}`}
|
||||||
|
className='user-popover__subtitle'
|
||||||
|
>
|
||||||
|
{attribute.name}
|
||||||
|
</strong>
|
||||||
|
<p
|
||||||
|
aria-labelledby={`user-popover__custom_attributes-title-${attribute.id}`}
|
||||||
|
className='user-popover__subtitle-text'
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>{attributeSections}</>
|
<>{attributeSections}</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {withRouter} from 'react-router-dom';
|
|||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import type {Dispatch} from 'redux';
|
import type {Dispatch} from 'redux';
|
||||||
|
|
||||||
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
import {getFirstAdminSetupComplete, getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
||||||
import {getProfiles} from 'mattermost-redux/actions/users';
|
import {getProfiles} from 'mattermost-redux/actions/users';
|
||||||
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
|
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
@@ -89,6 +89,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
initializeProducts,
|
initializeProducts,
|
||||||
handleLoginLogoutSignal,
|
handleLoginLogoutSignal,
|
||||||
redirectToOnboardingOrDefaultTeam,
|
redirectToOnboardingOrDefaultTeam,
|
||||||
|
getCustomProfileAttributeFields,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ describe('components/Root', () => {
|
|||||||
handleLoginLogoutSignal,
|
handleLoginLogoutSignal,
|
||||||
redirectToOnboardingOrDefaultTeam,
|
redirectToOnboardingOrDefaultTeam,
|
||||||
}, store.dispatch),
|
}, store.dispatch),
|
||||||
|
getCustomProfileAttributeFields: jest.fn(),
|
||||||
},
|
},
|
||||||
permalinkRedirectTeamName: 'myTeam',
|
permalinkRedirectTeamName: 'myTeam',
|
||||||
...{
|
...{
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
this.props.actions.migrateRecentEmojis();
|
this.props.actions.migrateRecentEmojis();
|
||||||
this.props.actions.loadRecentlyUsedCustomEmojis();
|
this.props.actions.loadRecentlyUsedCustomEmojis();
|
||||||
|
this.props.actions.getCustomProfileAttributeFields();
|
||||||
|
|
||||||
this.showLandingPageIfNecessary();
|
this.showLandingPageIfNecessary();
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {bindActionCreators} from 'redux';
|
|||||||
import type {Dispatch} from 'redux';
|
import type {Dispatch} from 'redux';
|
||||||
|
|
||||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||||
import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
|
||||||
import {
|
import {
|
||||||
updateMe,
|
updateMe,
|
||||||
sendVerificationEmail,
|
sendVerificationEmail,
|
||||||
setDefaultProfileImage,
|
setDefaultProfileImage,
|
||||||
uploadProfileImage,
|
uploadProfileImage,
|
||||||
saveCustomProfileAttribute,
|
saveCustomProfileAttribute,
|
||||||
|
getCustomProfileAttributeValues,
|
||||||
} from 'mattermost-redux/actions/users';
|
} from 'mattermost-redux/actions/users';
|
||||||
import {getConfig, getCustomProfileAttributes, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getCustomProfileAttributes, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
setDefaultProfileImage,
|
setDefaultProfileImage,
|
||||||
uploadProfileImage,
|
uploadProfileImage,
|
||||||
saveCustomProfileAttribute,
|
saveCustomProfileAttribute,
|
||||||
getCustomProfileAttributeFields,
|
getCustomProfileAttributeValues,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {Provider} from 'react-redux';
|
|||||||
import type {UserPropertyField} from '@mattermost/types/properties';
|
import type {UserPropertyField} from '@mattermost/types/properties';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
|
||||||
|
|
||||||
import configureStore from 'store';
|
import configureStore from 'store';
|
||||||
|
|
||||||
import {shallowWithIntl, mountWithIntl} from 'tests/helpers/intl-test-helper';
|
import {shallowWithIntl, mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
@@ -56,7 +54,7 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
|
|||||||
setDefaultProfileImage: jest.fn(),
|
setDefaultProfileImage: jest.fn(),
|
||||||
uploadProfileImage: jest.fn(),
|
uploadProfileImage: jest.fn(),
|
||||||
saveCustomProfileAttribute: jest.fn(),
|
saveCustomProfileAttribute: jest.fn(),
|
||||||
getCustomProfileAttributeFields: jest.fn(),
|
getCustomProfileAttributeValues: jest.fn(),
|
||||||
},
|
},
|
||||||
maxFileSize: 1024,
|
maxFileSize: 1024,
|
||||||
ldapPositionAttributeSet: false,
|
ldapPositionAttributeSet: false,
|
||||||
@@ -66,7 +64,7 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const customProfileAttribute: UserPropertyField = {
|
const customProfileAttribute: UserPropertyField = {
|
||||||
id: '1',
|
id: 'field1',
|
||||||
group_id: 'custom_profile_attributes',
|
group_id: 'custom_profile_attributes',
|
||||||
name: 'Test Attribute',
|
name: 'Test Attribute',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -196,65 +194,98 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should show Custom Attribute Field with no value', async () => {
|
test('should show Custom Attribute Field with no value', async () => {
|
||||||
(Client4.getUserCustomProfileAttributesValues as jest.Mock).mockImplementation(async () => {
|
const testUser = {...user, custom_profile_attributes: {}};
|
||||||
return {};
|
|
||||||
});
|
const props = {
|
||||||
const props = {...requiredProps, enableCustomProfileAttributes: true, customProfileAttributeFields: {1: customProfileAttribute}};
|
...requiredProps,
|
||||||
props.user = {...user};
|
enableCustomProfileAttributes: true,
|
||||||
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
|
user: testUser,
|
||||||
|
};
|
||||||
|
|
||||||
renderWithContext(<UserSettingsGeneral {...props}/>);
|
renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
|
|
||||||
|
expect(props.actions.getCustomProfileAttributeValues).not.toHaveBeenCalled();
|
||||||
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
||||||
expect(await screen.findByText('Click \'Edit\' to add your custom attribute'));
|
expect(await screen.findByText('Click \'Edit\' to add your custom attribute'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show Custom Attribute Field with empty value', async () => {
|
test('should show Custom Attribute Field with empty value', async () => {
|
||||||
(Client4.getUserCustomProfileAttributesValues as jest.Mock).mockImplementation(async () => {
|
const testUser = {...user, custom_profile_attributes: {field1: ''}};
|
||||||
return {
|
|
||||||
1: '',
|
const props = {
|
||||||
};
|
...requiredProps,
|
||||||
});
|
enableCustomProfileAttributes: true,
|
||||||
const props = {...requiredProps, enableCustomProfileAttributes: true, customProfileAttributeFields: {1: customProfileAttribute}};
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
props.user = {...user};
|
user: testUser,
|
||||||
|
};
|
||||||
|
|
||||||
renderWithContext(<UserSettingsGeneral {...props}/>);
|
renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
|
|
||||||
|
expect(props.actions.getCustomProfileAttributeValues).not.toHaveBeenCalled();
|
||||||
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
||||||
expect(await screen.findByText('Click \'Edit\' to add your custom attribute'));
|
expect(await screen.findByText('Click \'Edit\' to add your custom attribute'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show Custom Attribute Field with value set', async () => {
|
test('should show Custom Attribute Field with value', async () => {
|
||||||
(Client4.getUserCustomProfileAttributesValues as jest.Mock).mockImplementation(async () => {
|
const testUser = {...user, custom_profile_attributes: {field1: 'FieldOneValue'}};
|
||||||
return {1: 'Custom Attribute Value'};
|
|
||||||
});
|
const props = {
|
||||||
const props = {...requiredProps, enableCustomProfileAttributes: true, customProfileAttributeFields: {1: customProfileAttribute}};
|
...requiredProps,
|
||||||
props.user = {...user};
|
enableCustomProfileAttributes: true,
|
||||||
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
|
user: testUser,
|
||||||
|
};
|
||||||
|
|
||||||
renderWithContext(<UserSettingsGeneral {...props}/>);
|
renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
|
|
||||||
|
expect(props.actions.getCustomProfileAttributeValues).not.toHaveBeenCalled();
|
||||||
|
expect(await screen.findByText('FieldOneValue')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call getCustomProfileAttributeValues if users properties are null', async () => {
|
||||||
|
const testUser = {...user};
|
||||||
|
const props = {
|
||||||
|
...requiredProps,
|
||||||
|
enableCustomProfileAttributes: true,
|
||||||
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
|
actions: {...requiredProps.actions},
|
||||||
|
user: testUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {rerender} = renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
|
expect(props.actions.getCustomProfileAttributeValues).toHaveBeenCalledTimes(1);
|
||||||
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
expect(await screen.getByRole('button', {name: `${customProfileAttribute.name} Edit`})).toBeInTheDocument();
|
||||||
expect(await screen.findByText('Custom Attribute Value'));
|
|
||||||
|
props.user = {...testUser, custom_profile_attributes: {field1: 'FieldOneValue'}};
|
||||||
|
console.log(props.user);
|
||||||
|
rerender(<UserSettingsGeneral {...props}/>);
|
||||||
|
expect(props.actions.getCustomProfileAttributeValues).toHaveBeenCalledTimes(1);
|
||||||
|
expect(await screen.findByText('FieldOneValue')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show Custom Attribute Field editing with empty value', async () => {
|
test('should show Custom Attribute Field editing with empty value', async () => {
|
||||||
const props = {...requiredProps, enableCustomProfileAttributes: true, customProfileAttributeFields: {1: customProfileAttribute}};
|
const props = {
|
||||||
props.user = {...user};
|
...requiredProps,
|
||||||
props.activeSection = 'customAttribute_1';
|
enableCustomProfileAttributes: true,
|
||||||
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
|
user,
|
||||||
|
activeSection: 'customAttribute_field1',
|
||||||
|
};
|
||||||
|
|
||||||
renderWithContext(<UserSettingsGeneral {...props}/>);
|
renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
|
|
||||||
expect(await screen.getByRole('textbox', {name: `${customProfileAttribute.name}`})).toBeInTheDocument();
|
expect(await screen.getByRole('textbox', {name: `${customProfileAttribute.name}`})).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submitAttribute() should have called saveCustomProfileAttribute', async () => {
|
test('submitAttribute() should have called saveCustomProfileAttribute', async () => {
|
||||||
const saveCustomProfileAttribute = jest.fn().mockResolvedValue({1: 'Updated Value'});
|
const saveCustomProfileAttribute = jest.fn().mockResolvedValue({field1: 'Updated Value'});
|
||||||
const props = {
|
const props = {
|
||||||
...requiredProps,
|
...requiredProps,
|
||||||
enableCustomProfileAttributes: true,
|
enableCustomProfileAttributes: true,
|
||||||
actions: {...requiredProps.actions, saveCustomProfileAttribute},
|
actions: {...requiredProps.actions, saveCustomProfileAttribute},
|
||||||
customProfileAttributeFields: {1: customProfileAttribute},
|
customProfileAttributeFields: {field1: customProfileAttribute},
|
||||||
user: {...user},
|
user: {...user},
|
||||||
activeSection: 'customAttribute_1',
|
activeSection: 'customAttribute_field1',
|
||||||
};
|
};
|
||||||
|
|
||||||
renderWithContext(<UserSettingsGeneral {...props}/>);
|
renderWithContext(<UserSettingsGeneral {...props}/>);
|
||||||
@@ -266,6 +297,6 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
|
|||||||
userEvent.click(screen.getByRole('button', {name: 'Save'}));
|
userEvent.click(screen.getByRole('button', {name: 'Save'}));
|
||||||
|
|
||||||
expect(saveCustomProfileAttribute).toHaveBeenCalledTimes(1);
|
expect(saveCustomProfileAttribute).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCustomProfileAttribute).toHaveBeenCalledWith('user_id', '1', 'Updated Value');
|
expect(saveCustomProfileAttribute).toHaveBeenCalledWith('user_id', 'field1', 'Updated Value');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type {IDMappedObjects} from '@mattermost/types/utilities';
|
|||||||
|
|
||||||
import type {LogErrorOptions} from 'mattermost-redux/actions/errors';
|
import type {LogErrorOptions} from 'mattermost-redux/actions/errors';
|
||||||
import {LogErrorBarMode} from 'mattermost-redux/actions/errors';
|
import {LogErrorBarMode} from 'mattermost-redux/actions/errors';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||||
|
|
||||||
@@ -120,7 +119,7 @@ export type Props = {
|
|||||||
setDefaultProfileImage: (id: string) => void;
|
setDefaultProfileImage: (id: string) => void;
|
||||||
uploadProfileImage: (id: string, file: File) => Promise<ActionResult>;
|
uploadProfileImage: (id: string, file: File) => Promise<ActionResult>;
|
||||||
saveCustomProfileAttribute: (userID: string, attributeID: string, attributeValue: string) => Promise<ActionResult<Record<string, string>>>;
|
saveCustomProfileAttribute: (userID: string, attributeID: string, attributeValue: string) => Promise<ActionResult<Record<string, string>>>;
|
||||||
getCustomProfileAttributeFields: () => Promise<ActionResult>;
|
getCustomProfileAttributeValues: (userID: string) => Promise<ActionResult<Record<string, string>>>;
|
||||||
};
|
};
|
||||||
requireEmailVerification?: boolean;
|
requireEmailVerification?: boolean;
|
||||||
ldapFirstNameAttributeSet?: boolean;
|
ldapFirstNameAttributeSet?: boolean;
|
||||||
@@ -165,14 +164,8 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.enableCustomProfileAttributes) {
|
if (this.props.enableCustomProfileAttributes && !this.props.user.custom_profile_attributes) {
|
||||||
const fetchValues = async () => {
|
this.props.actions.getCustomProfileAttributeValues(this.props.user.id);
|
||||||
const response = await Client4.getUserCustomProfileAttributesValues(this.props.user.id);
|
|
||||||
this.setState({customAttributeValues: response});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.props.actions.getCustomProfileAttributeFields();
|
|
||||||
fetchValues();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,10 +488,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
setupInitialState(props: Props) {
|
setupInitialState(props: Props) {
|
||||||
const user = props.user;
|
const user = props.user;
|
||||||
let cav = {};
|
|
||||||
if (this.state !== undefined) {
|
|
||||||
cav = this.state.customAttributeValues;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
firstName: user.first_name,
|
firstName: user.first_name,
|
||||||
@@ -514,7 +503,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
sectionIsSaving: false,
|
sectionIsSaving: false,
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
serverError: '',
|
serverError: '',
|
||||||
customAttributeValues: cav,
|
customAttributeValues: user.custom_profile_attributes || {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,12 +1321,11 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createCustomAttributeSection = () => {
|
createCustomAttributeSection = () => {
|
||||||
if (this.props.customProfileAttributeFields == null) {
|
if (!this.props.enableCustomProfileAttributes || this.props.customProfileAttributeFields == null) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributeSections = Object.values(this.props.customProfileAttributeFields).map((attribute) => {
|
const attributeSections = Object.values(this.props.customProfileAttributeFields).map((attribute) => {
|
||||||
const attributeValue = this.state.customAttributeValues?.[attribute.id] ?? '';
|
|
||||||
const sectionName = 'customAttribute_' + attribute.id;
|
const sectionName = 'customAttribute_' + attribute.id;
|
||||||
const active = this.props.activeSection === sectionName;
|
const active = this.props.activeSection === sectionName;
|
||||||
let max = null;
|
let max = null;
|
||||||
@@ -1365,7 +1353,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
className='form-control'
|
className='form-control'
|
||||||
type='text'
|
type='text'
|
||||||
onChange={this.updateAttribute}
|
onChange={this.updateAttribute}
|
||||||
value={attributeValue}
|
value={this.state.customAttributeValues[attribute.id] || ''}
|
||||||
maxLength={Constants.MAX_CUSTOM_ATTRIBUTE_LENGTH}
|
maxLength={Constants.MAX_CUSTOM_ATTRIBUTE_LENGTH}
|
||||||
autoCapitalize='off'
|
autoCapitalize='off'
|
||||||
onFocus={Utils.moveCursorToEnd}
|
onFocus={Utils.moveCursorToEnd}
|
||||||
@@ -1399,6 +1387,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let describe: JSX.Element|string = '';
|
let describe: JSX.Element|string = '';
|
||||||
|
const attributeValue = this.props.user.custom_profile_attributes?.[attribute.id];
|
||||||
if (attributeValue) {
|
if (attributeValue) {
|
||||||
describe = attributeValue;
|
describe = attributeValue;
|
||||||
} else {
|
} else {
|
||||||
@@ -1536,7 +1525,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
const usernameSection = this.createUsernameSection();
|
const usernameSection = this.createUsernameSection();
|
||||||
const positionSection = this.createPositionSection();
|
const positionSection = this.createPositionSection();
|
||||||
const emailSection = this.createEmailSection();
|
const emailSection = this.createEmailSection();
|
||||||
const customProperiesSection = this.createCustomAttributeSection();
|
const customAttributeSection = this.createCustomAttributeSection();
|
||||||
const pictureSection = this.createPictureSection();
|
const pictureSection = this.createPictureSection();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1576,7 +1565,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
|
|||||||
<div className='divider-light'/>
|
<div className='divider-light'/>
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<div className='divider-light'/>
|
<div className='divider-light'/>
|
||||||
{customProperiesSection}
|
{customAttributeSection}
|
||||||
{pictureSection}
|
{pictureSection}
|
||||||
<div className='divider-dark'/>
|
<div className='divider-dark'/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export default keyMirror({
|
|||||||
CLIENT_LICENSE_RECEIVED: null,
|
CLIENT_LICENSE_RECEIVED: null,
|
||||||
CLIENT_LICENSE_RESET: null,
|
CLIENT_LICENSE_RESET: null,
|
||||||
|
|
||||||
CUSTOM_PROFILE_ATTRIBUTES_RECEIVED: null,
|
CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED: null,
|
||||||
|
CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED: null,
|
||||||
|
CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED: null,
|
||||||
|
CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED: null,
|
||||||
|
|
||||||
LOG_CLIENT_ERROR_REQUEST: null,
|
LOG_CLIENT_ERROR_REQUEST: null,
|
||||||
LOG_CLIENT_ERROR_SUCCESS: null,
|
LOG_CLIENT_ERROR_SUCCESS: null,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default keyMirror({
|
|||||||
RECEIVED_TERMS_OF_SERVICE_STATUS: null,
|
RECEIVED_TERMS_OF_SERVICE_STATUS: null,
|
||||||
RECEIVED_PROFILE: null,
|
RECEIVED_PROFILE: null,
|
||||||
RECEIVED_PROFILES: null,
|
RECEIVED_PROFILES: null,
|
||||||
|
RECEIVED_CPA_VALUES: null,
|
||||||
RECEIVED_PROFILES_LIST: null,
|
RECEIVED_PROFILES_LIST: null,
|
||||||
RECEIVED_PROFILES_IN_TEAM: null,
|
RECEIVED_PROFILES_IN_TEAM: null,
|
||||||
RECEIVED_PROFILE_IN_TEAM: null,
|
RECEIVED_PROFILE_IN_TEAM: null,
|
||||||
|
|||||||
@@ -46,22 +46,10 @@ export function getLicenseConfig() {
|
|||||||
export function getCustomProfileAttributeFields() {
|
export function getCustomProfileAttributeFields() {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: Client4.getCustomProfileAttributeFields,
|
clientFunc: Client4.getCustomProfileAttributeFields,
|
||||||
onSuccess: [GeneralTypes.CUSTOM_PROFILE_ATTRIBUTES_RECEIVED],
|
onSuccess: [GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomProfileAttributeValues(userID: string) {
|
|
||||||
return async () => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = await Client4.getUserCustomProfileAttributesValues(userID);
|
|
||||||
} catch (error) {
|
|
||||||
return {error};
|
|
||||||
}
|
|
||||||
return {data};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logClientError(message: string, level = LogLevel.Error) {
|
export function logClientError(message: string, level = LogLevel.Error) {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: Client4.logClientError,
|
clientFunc: Client4.logClientError,
|
||||||
|
|||||||
@@ -1776,4 +1776,31 @@ describe('Actions.Users', () => {
|
|||||||
expect(profiles).toBe(originalState.entities.users.profiles);
|
expect(profiles).toBe(originalState.entities.users.profiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getCustomProfileAttributeValues', async () => {
|
||||||
|
const userID = 'user1';
|
||||||
|
nock(Client4.getUserRoute(userID) + '/custom_profile_attributes').
|
||||||
|
get('').
|
||||||
|
query(true).
|
||||||
|
reply(200, {field1: 'value1', field2: 'value2'});
|
||||||
|
|
||||||
|
const originalState = {
|
||||||
|
entities: {
|
||||||
|
users: {
|
||||||
|
profiles: {
|
||||||
|
user1: {id: userID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store = configureStore(originalState);
|
||||||
|
|
||||||
|
await store.dispatch(Actions.getCustomProfileAttributeValues(userID));
|
||||||
|
const customProfileAttributeValues = store.getState().entities.users.profiles[userID].custom_profile_attributes;
|
||||||
|
|
||||||
|
// Check a few basic fields since they may change over time
|
||||||
|
expect(Object.keys(customProfileAttributeValues).length).toEqual(2);
|
||||||
|
expect(customProfileAttributeValues.field1).toEqual('value1');
|
||||||
|
expect(customProfileAttributeValues.field2).toEqual('value2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -476,6 +476,24 @@ export function getMe(): ActionFuncAsync<UserProfile> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCustomProfileAttributeValues(userID: string): ActionFuncAsync<Record<string, string>> {
|
||||||
|
return async (dispatch) => {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await Client4.getUserCustomProfileAttributesValues(userID);
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UserTypes.RECEIVED_CPA_VALUES,
|
||||||
|
data: {userID, customAttributeValues: data},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {data};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function updateMyTermsOfServiceStatus(termsOfServiceId: string, accepted: boolean): ActionFuncAsync {
|
export function updateMyTermsOfServiceStatus(termsOfServiceId: string, accepted: boolean): ActionFuncAsync {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const response = await dispatch(bindClientFunc({
|
const response = await dispatch(bindClientFunc({
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ describe('reducers.entities.general', () => {
|
|||||||
expect(actualState.firstAdminVisitMarketplaceStatus).toEqual(expectedState);
|
expect(actualState.firstAdminVisitMarketplaceStatus).toEqual(expectedState);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CUSTOM_PROFILE_ATTRIBUTES_RECEIVED, empty initial state', () => {
|
it('CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED, empty initial state', () => {
|
||||||
const state = {};
|
const state = {};
|
||||||
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
||||||
const action = {
|
const action = {
|
||||||
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTES_RECEIVED,
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED,
|
||||||
data: [testAttributeOne],
|
data: [testAttributeOne],
|
||||||
};
|
};
|
||||||
const expectedState = {[testAttributeOne.id]: testAttributeOne} as ReducerState['customProfileAttributes'];
|
const expectedState = {[testAttributeOne.id]: testAttributeOne} as ReducerState['customProfileAttributes'];
|
||||||
@@ -64,7 +64,7 @@ describe('reducers.entities.general', () => {
|
|||||||
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CUSTOM_PROFILE_ATTRIBUTES_RECEIVED, attributes are completely replaced', () => {
|
it('CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED, attributes are completely replaced', () => {
|
||||||
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
||||||
const testAttributeTwo = {id: '456', name: 'test attribute two', type: 'text'};
|
const testAttributeTwo = {id: '456', name: 'test attribute two', type: 'text'};
|
||||||
const state = {[testAttributeOne.id]: testAttributeOne, [testAttributeTwo.id]: testAttributeTwo};
|
const state = {[testAttributeOne.id]: testAttributeOne, [testAttributeTwo.id]: testAttributeTwo};
|
||||||
@@ -72,7 +72,7 @@ describe('reducers.entities.general', () => {
|
|||||||
const updatedAttributeOne = {id: '123', name: 'new name value', type: 'text'};
|
const updatedAttributeOne = {id: '123', name: 'new name value', type: 'text'};
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTES_RECEIVED,
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED,
|
||||||
data: [updatedAttributeOne],
|
data: [updatedAttributeOne],
|
||||||
};
|
};
|
||||||
const expectedState = {[updatedAttributeOne.id]: updatedAttributeOne};
|
const expectedState = {[updatedAttributeOne.id]: updatedAttributeOne};
|
||||||
@@ -80,5 +80,44 @@ describe('reducers.entities.general', () => {
|
|||||||
const actualState = reducer({customProfileAttributes: state} as ReducerState, action);
|
const actualState = reducer({customProfileAttributes: state} as ReducerState, action);
|
||||||
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED', () => {
|
||||||
|
const state = {};
|
||||||
|
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
||||||
|
const action = {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED,
|
||||||
|
data: testAttributeOne,
|
||||||
|
};
|
||||||
|
const expectedState = {[testAttributeOne.id]: testAttributeOne} as ReducerState['customProfileAttributes'];
|
||||||
|
const actualState = reducer({customProfileAttributes: state} as ReducerState, action);
|
||||||
|
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED', () => {
|
||||||
|
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
||||||
|
const state = {[testAttributeOne.id]: testAttributeOne};
|
||||||
|
|
||||||
|
const renamedAttributeOne = {...testAttributeOne, name: 'renamed attribute'};
|
||||||
|
const action = {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED,
|
||||||
|
data: renamedAttributeOne,
|
||||||
|
};
|
||||||
|
const expectedState = {[testAttributeOne.id]: renamedAttributeOne} as ReducerState['customProfileAttributes'];
|
||||||
|
const actualState = reducer({customProfileAttributes: state} as ReducerState, action);
|
||||||
|
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED', () => {
|
||||||
|
const testAttributeOne = {id: '123', name: 'test attribute', type: 'text'};
|
||||||
|
const state = {[testAttributeOne.id]: testAttributeOne};
|
||||||
|
|
||||||
|
const action = {
|
||||||
|
type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED,
|
||||||
|
data: testAttributeOne.id,
|
||||||
|
};
|
||||||
|
const expectedState = {} as ReducerState['customProfileAttributes'];
|
||||||
|
const actualState = reducer({customProfileAttributes: state} as ReducerState, action);
|
||||||
|
expect(actualState.customProfileAttributes).toEqual(expectedState);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,13 +40,30 @@ function license(state: ClientLicense = {}, action: MMReduxAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function customProfileAttributes(state: IDMappedObjects<UserPropertyField> = {}, action: MMReduxAction) {
|
function customProfileAttributes(state: IDMappedObjects<UserPropertyField> = {}, action: MMReduxAction) {
|
||||||
const data: UserPropertyField[] = action.data;
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case GeneralTypes.CUSTOM_PROFILE_ATTRIBUTES_RECEIVED:
|
case GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELDS_RECEIVED: {
|
||||||
|
const data: UserPropertyField[] = action.data;
|
||||||
return data.reduce<IDMappedObjects<UserPropertyField>>((acc, field) => {
|
return data.reduce<IDMappedObjects<UserPropertyField>>((acc, field) => {
|
||||||
acc[field.id] = field;
|
acc[field.id] = field;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
}
|
||||||
|
case GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED: {
|
||||||
|
const nextState = {...state};
|
||||||
|
const fieldId = action.data;
|
||||||
|
if (Object.hasOwn(nextState, fieldId)) {
|
||||||
|
Reflect.deleteProperty(nextState, fieldId);
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED:
|
||||||
|
case GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_PATCHED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.data.id]: action.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1053,6 +1053,39 @@ describe('Reducers.users', () => {
|
|||||||
expect(newProfiles.second_user_id).toEqual(secondUser);
|
expect(newProfiles.second_user_id).toEqual(secondUser);
|
||||||
expect(newProfiles.third_user_id).toEqual(thirdUser);
|
expect(newProfiles.third_user_id).toEqual(thirdUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('UserTypes.RECEIVED_CPA_VALUES, should merge existing users custom attributes', () => {
|
||||||
|
const firstUser = TestHelper.getUserMock({id: 'first_user_id'});
|
||||||
|
const secondUser = TestHelper.getUserMock({id: 'second_user_id'});
|
||||||
|
const state = {
|
||||||
|
profiles: {
|
||||||
|
first_user_id: firstUser,
|
||||||
|
second_user_id: secondUser,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const action = {
|
||||||
|
type: UserTypes.RECEIVED_CPA_VALUES,
|
||||||
|
data: {
|
||||||
|
userID: 'first_user_id',
|
||||||
|
customAttributeValues: {field1: 'value1'},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const {profiles: newProfiles} = reducer(state as unknown as ReducerState, action);
|
||||||
|
|
||||||
|
expect(newProfiles.first_user_id.custom_profile_attributes!.field1).toEqual('value1');
|
||||||
|
|
||||||
|
// update field
|
||||||
|
const updateAction = {
|
||||||
|
type: UserTypes.RECEIVED_CPA_VALUES,
|
||||||
|
data: {
|
||||||
|
userID: 'first_user_id',
|
||||||
|
customAttributeValues: {field1: 'updatedValue'},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const {profiles: updatedProfiles} = reducer(state as unknown as ReducerState, updateAction);
|
||||||
|
|
||||||
|
expect(updatedProfiles.first_user_id.custom_profile_attributes!.field1).toEqual('updatedValue');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PROFILE_NO_LONGER_VISIBLE should remove references to users from state', () => {
|
test('PROFILE_NO_LONGER_VISIBLE should remove references to users from state', () => {
|
||||||
|
|||||||
@@ -227,6 +227,15 @@ function profiles(state: UsersState['profiles'] = {}, action: MMReduxAction) {
|
|||||||
|
|
||||||
return receiveUserProfile(state, user);
|
return receiveUserProfile(state, user);
|
||||||
}
|
}
|
||||||
|
case UserTypes.RECEIVED_CPA_VALUES: {
|
||||||
|
const {userID, customAttributeValues} = action.data;
|
||||||
|
const existingProfile = state[userID];
|
||||||
|
if (!existingProfile) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const profileAttributes = {...existingProfile.custom_profile_attributes, ...customAttributeValues};
|
||||||
|
return receiveUserProfile(state, {...existingProfile, custom_profile_attributes: profileAttributes});
|
||||||
|
}
|
||||||
case UserTypes.RECEIVED_PROFILES_LIST: {
|
case UserTypes.RECEIVED_PROFILES_LIST: {
|
||||||
const users: UserProfile[] = action.data;
|
const users: UserProfile[] = action.data;
|
||||||
|
|
||||||
|
|||||||
@@ -664,6 +664,10 @@ export const SocketEvents = {
|
|||||||
SCHEDULED_POST_DELETED: 'scheduled_post_deleted',
|
SCHEDULED_POST_DELETED: 'scheduled_post_deleted',
|
||||||
PERSISTENT_NOTIFICATION_TRIGGERED: 'persistent_notification_triggered',
|
PERSISTENT_NOTIFICATION_TRIGGERED: 'persistent_notification_triggered',
|
||||||
HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED: 'hosted_customer_signup_progress_updated',
|
HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED: 'hosted_customer_signup_progress_updated',
|
||||||
|
CPA_FIELD_CREATED: 'custom_profile_attributes_field_created',
|
||||||
|
CPA_FIELD_UPDATED: 'custom_profile_attributes_field_updated',
|
||||||
|
CPA_FIELD_DELETED: 'custom_profile_attributes_field_deleted',
|
||||||
|
CPA_VALUES_UPDATED: 'custom_profile_attributes_values_updated',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TutorialSteps = {
|
export const TutorialSteps = {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export type UserProfile = {
|
|||||||
terms_of_service_create_at: number;
|
terms_of_service_create_at: number;
|
||||||
remote_id?: string;
|
remote_id?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
custom_profile_attributes?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserProfileWithLastViewAt = UserProfile & {
|
export type UserProfileWithLastViewAt = UserProfile & {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user