Skip to content

Commit

Permalink
Get options for post options based on the post channel (mattermost#8439)
Browse files Browse the repository at this point in the history
* Get options for post options based on the post channel

* Only fetch when the menu is actually opened, and fix type error

* Address feedback and prepare for RHS state

* Add redux RHS bindings related code

* Extract get bindings from the component

* Address feedback

* Fix tests

* Add more logic into a selector

Co-authored-by: Mattermod <[email protected]>
  • Loading branch information
larkox and mattermod authored Sep 23, 2021
1 parent c4efa2e commit 5d8c2c3
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 9 deletions.
17 changes: 16 additions & 1 deletion actions/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import {Client4} from 'mattermost-redux/client';
import {Action, ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions';
import {AppCallResponse, AppForm, AppCallType, AppCallRequest, AppContext} from 'mattermost-redux/types/apps';
import {AppCallResponse, AppForm, AppCallType, AppCallRequest, AppContext, AppBinding} from 'mattermost-redux/types/apps';
import {AppCallTypes, AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import {Post} from 'mattermost-redux/types/posts';
import {CommandArgs} from 'mattermost-redux/types/integrations';
Expand Down Expand Up @@ -90,6 +90,21 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
};
}

export function makeFetchBindings(location: string): (userId: string, channelId: string, teamId: string) => ActionFunc {
return (userId: string, channelId: string, teamId: string): ActionFunc => {
return async () => {
try {
const allBindings = await Client4.getAppsBindings(userId, channelId, teamId);
const headerBindings = allBindings.filter((b) => b.location === location);
const bindings = headerBindings.reduce((accum: AppBinding[], current: AppBinding) => accum.concat(current.bindings || []), []);
return {data: bindings};
} catch {
return {data: []};
}
};
};
}

export function openAppsModal(form: AppForm, call: AppCallRequest): Action {
return openModal({
modalId: ModalIdentifiers.APPS_MODAL,
Expand Down
1 change: 1 addition & 0 deletions components/dot_menu/dot_menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('components/dot_menu/DotMenu', () => {
doAppCall: jest.fn(),
postEphemeralCallResponseForPost: jest.fn(),
setThreadFollow: jest.fn(),
fetchBindings: jest.fn(),
},
canEdit: false,
canDelete: false,
Expand Down
30 changes: 26 additions & 4 deletions components/dot_menu/dot_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Props = {
enableEmojiPicker?: boolean; // TechDebt: Made non-mandatory while converting to typescript
channelIsArchived?: boolean; // TechDebt: Made non-mandatory while converting to typescript
currentTeamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript
appBindings?: AppBinding[];
appBindings: AppBinding[] | null;
appsEnabled: boolean;

/**
Expand Down Expand Up @@ -111,6 +111,11 @@ type Props = {
*/
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;

/**
* Function to get the post menu bindings for this post.
*/
fetchBindings: (userId: string, channelId: string, teamId: string) => Promise<{data: AppBinding[]}>;

}; // TechDebt: Made non-mandatory while converting to typescript

canEdit: boolean;
Expand All @@ -127,6 +132,7 @@ type State = {
openUp: boolean;
canEdit: boolean;
canDelete: boolean;
appBindings?: AppBinding[];
}

export class DotMenuClass extends React.PureComponent<Props, State> {
Expand Down Expand Up @@ -175,11 +181,21 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
this.disableCanEditPostByTime();
}

componentDidUpdate(prevProps: Props) {
if (!this.state.appBindings && this.props.isMenuOpen && !prevProps.isMenuOpen) {
this.fetchBindings();
}
}

static getDerivedStateFromProps(props: Props) {
return {
const state: Partial<State> = {
canEdit: props.canEdit && !props.isReadOnly,
canDelete: props.canDelete && !props.isReadOnly,
};
if (props.appBindings) {
state.appBindings = props.appBindings;
}
return state;
}

componentWillUnmount() {
Expand Down Expand Up @@ -358,6 +374,12 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
}
}

fetchBindings = () => {
this.props.actions.fetchBindings(this.props.userId, this.props.post.channel_id, this.props.currentTeamId).then(({data}) => {
this.setState({appBindings: data});
});
}

render() {
const isSystemMessage = PostUtils.isSystemMessage(this.props.post);
const isMobile = Utils.isMobile();
Expand Down Expand Up @@ -394,8 +416,8 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
}) || [];

let appBindings = [] as JSX.Element[];
if (this.props.appsEnabled && this.props.appBindings) {
appBindings = this.props.appBindings.map((item) => {
if (this.props.appsEnabled && this.state.appBindings) {
appBindings = this.state.appBindings.map((item) => {
let icon: JSX.Element | undefined;
if (item.icon) {
icon = (<img src={item.icon}/>);
Expand Down
1 change: 1 addition & 0 deletions components/dot_menu/dot_menu_empty.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => {
doAppCall: jest.fn(),
postEphemeralCallResponseForPost: jest.fn(),
setThreadFollow: jest.fn(),
fetchBindings: jest.fn(),
},
canEdit: false,
canDelete: false,
Expand Down
1 change: 1 addition & 0 deletions components/dot_menu/dot_menu_mobile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => {
doAppCall: jest.fn(),
postEphemeralCallResponseForPost: jest.fn(),
setThreadFollow: jest.fn(),
fetchBindings: jest.fn(),
},
canEdit: false,
canDelete: false,
Expand Down
18 changes: 14 additions & 4 deletions components/dot_menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId, getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {appsEnabled, makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps';
import {appsEnabled, makeGetPostOptionBinding} from 'mattermost-redux/selectors/entities/apps';
import {getThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
Expand All @@ -19,6 +19,7 @@ import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isCombinedUserActivityPost} from 'mattermost-redux/utils/post_list';

import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
import {AppBinding} from 'mattermost-redux/types/apps';

import {Post} from 'mattermost-redux/types/posts';

Expand All @@ -28,7 +29,7 @@ import {setThreadFollow} from 'mattermost-redux/actions/threads';
import {GlobalState} from 'types/store';

import {openModal} from 'actions/views/modals';
import {doAppCall, postEphemeralCallResponseForPost} from 'actions/apps';
import {doAppCall, makeFetchBindings, postEphemeralCallResponseForPost} from 'actions/apps';

import {
flagPost,
Expand Down Expand Up @@ -60,7 +61,11 @@ type Props = {
location?: ComponentProps<typeof DotMenu>['location'];
};

const getPostMenuBindings = makeAppBindingsSelector(AppBindingLocations.POST_MENU_ITEM);
const emptyBindings: AppBinding[] = [];

const getPostOptionBinding = makeGetPostOptionBinding();

const fetchBindings = makeFetchBindings(AppBindingLocations.POST_MENU_ITEM);

function mapStateToProps(state: GlobalState, ownProps: Props) {
const {post} = ownProps;
Expand Down Expand Up @@ -103,7 +108,10 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {

const apps = appsEnabled(state);
const showBindings = apps && !systemMessage && !isCombinedUserActivityPost(post.id);
const appBindings = showBindings ? getPostMenuBindings(state) : undefined;
let appBindings: AppBinding[] | null = emptyBindings;
if (showBindings) {
appBindings = getPostOptionBinding(state, ownProps.location);
}

return {
channelIsArchived: isArchivedChannel(channel),
Expand Down Expand Up @@ -138,6 +146,7 @@ type Actions = {
doAppCall: DoAppCall;
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
fetchBindings: (userId: string, channelId: string, teamId: string) => Promise<{data: AppBinding[]}>;
}

function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
Expand All @@ -153,6 +162,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
doAppCall,
postEphemeralCallResponseForPost,
setThreadFollow,
fetchBindings,
}, dispatch),
};
}
Expand Down
25 changes: 25 additions & 0 deletions packages/mattermost-redux/src/selectors/entities/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {AppBinding} from 'mattermost-redux/types/apps';
import {ClientConfig} from 'mattermost-redux/types/config';

import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
import {Locations} from 'utils/constants';

// This file's contents belong to the Apps Framework feature.
// Apps Framework feature is experimental, and the contents of this file are
Expand Down Expand Up @@ -60,3 +62,26 @@ export const getAppCommandForm = (state: GlobalState, location: string) => {
export const getAppRHSCommandForm = (state: GlobalState, location: string) => {
return state.entities.apps.rhs.forms[location];
};

export function makeGetPostOptionBinding(): (state: GlobalState, location?: string) => AppBinding[] | null {
const centerBindingsSelector = makeAppBindingsSelector(AppBindingLocations.POST_MENU_ITEM);
const rhsBindingsSelector = makeRHSAppBindingSelector(AppBindingLocations.POST_MENU_ITEM);
return createSelector(
'postOptionsBindings',
centerBindingsSelector,
rhsBindingsSelector,
(state: GlobalState, location?: string) => location,
(centerBindings: AppBinding[], rhsBindings: AppBinding[], location?: string) => {
switch (location) {
case Locations.RHS_ROOT:
case Locations.RHS_COMMENT:
return rhsBindings;
case Locations.SEARCH:
return null;
case Locations.CENTER:
default:
return centerBindings;
}
},
);
}

0 comments on commit 5d8c2c3

Please sign in to comment.