mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
Data spillage caching (#35016)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Elliminated loadFlaggedPost * Elliminated loadChannel * WIP * Cached teams data * Always use Set in dataloader * minor cleanup * test: add tests for content flagging actions * Added tests * Handled user logout event in content flagging reducers * Fixed content flagging playwrite tests --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
0ec4a474d5
commit
a69653ce2e
20 changed files with 938 additions and 189 deletions
|
|
@ -55,9 +55,6 @@ export default class ContentReviewPage {
|
|||
}
|
||||
|
||||
async waitForPageLoaded() {
|
||||
await this.page.waitForResponse(
|
||||
(res) => res.url().includes('as_content_reviewer=true') && res.status() === 200,
|
||||
);
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
this.ensureReportCardSet();
|
||||
|
|
@ -129,10 +126,6 @@ export default class ContentReviewPage {
|
|||
}
|
||||
|
||||
async waitForRHSVisible() {
|
||||
await this.page.waitForResponse(
|
||||
(res) => res.url().includes('as_content_reviewer=true') && res.status() === 200,
|
||||
);
|
||||
|
||||
const gotIt = this.page.getByRole('button', {name: 'Got it'});
|
||||
if (await gotIt.isVisible()) {
|
||||
await gotIt.click();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import type {
|
||||
ContentFlaggingChannelRequestIdentifier,
|
||||
ContentFlaggingTeamRequestIdentifier,
|
||||
} from 'mattermost-redux/actions/content_flagging';
|
||||
import {
|
||||
loadContentFlaggingTeam,
|
||||
loadContentFlaggingChannel,
|
||||
loadFlaggedPost,
|
||||
} from 'mattermost-redux/actions/content_flagging';
|
||||
import {
|
||||
getContentFlaggingChannel,
|
||||
getContentFlaggingTeam,
|
||||
getFlaggedPost,
|
||||
} from 'mattermost-redux/selectors/entities/content_flagging';
|
||||
|
||||
import {makeUseEntity} from 'components/common/hooks/useEntity';
|
||||
|
||||
export const useGetFlaggedPost = makeUseEntity<Post | undefined>({
|
||||
name: 'useGetFlaggedPost',
|
||||
fetch: loadFlaggedPost,
|
||||
selector: getFlaggedPost,
|
||||
});
|
||||
|
||||
export const useGetContentFlaggingChannel = makeUseEntity<Channel | undefined, ContentFlaggingChannelRequestIdentifier>({
|
||||
name: 'useGetContentFlaggingChannel',
|
||||
fetch: loadContentFlaggingChannel,
|
||||
selector: getContentFlaggingChannel,
|
||||
});
|
||||
|
||||
export const useGetContentFlaggingTeam = makeUseEntity<Team | undefined, ContentFlaggingTeamRequestIdentifier>({
|
||||
name: 'useGetContentFlaggingTeam',
|
||||
fetch: loadContentFlaggingTeam,
|
||||
selector: getContentFlaggingTeam,
|
||||
});
|
||||
|
|
@ -96,6 +96,17 @@ describe('components/post_view/data_spillage_report/DataSpillageReport', () => {
|
|||
[reportedPostTeam.id]: reportedPostTeam,
|
||||
},
|
||||
},
|
||||
contentFlagging: {
|
||||
flaggedPosts: {
|
||||
[reportedPost.id]: reportedPost,
|
||||
},
|
||||
channels: {
|
||||
[reportedPostChannel.id]: reportedPostChannel,
|
||||
},
|
||||
teams: {
|
||||
[reportedPostTeam.id]: reportedPostTeam,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import {ContentFlaggingStatus} from '@mattermost/types/content_flagging';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {NameMappedPropertyFields, PropertyValue} from '@mattermost/types/properties';
|
||||
|
|
@ -14,6 +12,7 @@ import {Client4} from 'mattermost-redux/client';
|
|||
import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import AtMention from 'components/at_mention';
|
||||
import {useGetContentFlaggingChannel, useGetContentFlaggingTeam, useGetFlaggedPost} from 'components/common/hooks/content_flagging';
|
||||
import {useContentFlaggingFields, usePostContentFlaggingValues} from 'components/common/hooks/useContentFlaggingFields';
|
||||
import {useUser} from 'components/common/hooks/useUser';
|
||||
import DataSpillageAction from 'components/post_view/data_spillage_report/data_spillage_actions/data_spillage_actions';
|
||||
|
|
@ -59,44 +58,15 @@ type Props = {
|
|||
|
||||
export function DataSpillageReport({post, isRHS}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const loaded = useRef(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const reportedPostId = post.props.reported_post_id as string;
|
||||
|
||||
const naturalPropertyFields = useContentFlaggingFields('fetch');
|
||||
const naturalPropertyValues = usePostContentFlaggingValues(reportedPostId);
|
||||
|
||||
const [reportedPost, setReportedPost] = useState<Post>();
|
||||
const [channel, setChannel] = useState<Channel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChannel = async () => {
|
||||
if (reportedPost && reportedPost.channel_id) {
|
||||
const fetchedChannel = await getChannel(reportedPostId)(reportedPost.channel_id);
|
||||
setChannel(fetchedChannel);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChannel();
|
||||
}, [reportedPost, reportedPostId]);
|
||||
|
||||
useEffect(() => {
|
||||
const work = async () => {
|
||||
if (!loaded.current && !reportedPost) {
|
||||
// We need to obtain the post directly from action bypassing the selectors
|
||||
// because the post might be soft-deleted and the post reducers do not store deleted posts
|
||||
// in the store.
|
||||
const post = await loadFlaggedPost(reportedPostId);
|
||||
if (post) {
|
||||
setReportedPost(post);
|
||||
loaded.current = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
work();
|
||||
}, [dispatch, reportedPost, reportedPostId]);
|
||||
const reportedPost = useGetFlaggedPost(reportedPostId);
|
||||
const channel = useGetContentFlaggingChannel({flaggedPostId: reportedPostId, channelId: reportedPost?.channel_id});
|
||||
const team = useGetContentFlaggingTeam({flaggedPostId: reportedPostId, teamId: channel?.team_id});
|
||||
|
||||
const propertyFields = useMemo((): NameMappedPropertyFields => {
|
||||
if (!naturalPropertyFields || !Object.keys(naturalPropertyFields).length) {
|
||||
|
|
@ -139,22 +109,26 @@ export function DataSpillageReport({post, isRHS}: Props) {
|
|||
const mode = isRHS ? 'full' : 'short';
|
||||
|
||||
const metadata = useMemo<PropertiesCardViewMetadata>(() => {
|
||||
const fieldMetadata = {
|
||||
const fieldMetadata: PropertiesCardViewMetadata = {
|
||||
post_preview: {
|
||||
getPost: loadFlaggedPost,
|
||||
post: reportedPost,
|
||||
fetchDeletedPost: true,
|
||||
getChannel: getChannel(reportedPostId),
|
||||
getTeam: getTeam(reportedPostId),
|
||||
generateFileDownloadUrl: generateFileDownloadUrl(reportedPostId),
|
||||
channel,
|
||||
team,
|
||||
generateFileDownloadUrl:
|
||||
generateFileDownloadUrl(reportedPostId),
|
||||
},
|
||||
reporting_comment: {
|
||||
placeholder: formatMessage({id: 'data_spillage_report_post.reporting_comment.placeholder', defaultMessage: 'No comment'}),
|
||||
placeholder: formatMessage({
|
||||
id: 'data_spillage_report_post.reporting_comment.placeholder',
|
||||
defaultMessage: 'No comment',
|
||||
}),
|
||||
},
|
||||
team: {
|
||||
getTeam: getTeam(reportedPostId),
|
||||
team,
|
||||
},
|
||||
channel: {
|
||||
getChannel: getChannel(reportedPostId),
|
||||
channel,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -168,7 +142,7 @@ export function DataSpillageReport({post, isRHS}: Props) {
|
|||
}
|
||||
|
||||
return fieldMetadata;
|
||||
}, [channel, formatMessage, reportedPostId]);
|
||||
}, [channel, formatMessage, reportedPost, reportedPostId, team]);
|
||||
|
||||
const footer = useMemo(() => {
|
||||
if (isRHS) {
|
||||
|
|
@ -219,10 +193,6 @@ export function DataSpillageReport({post, isRHS}: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
async function loadFlaggedPost(flaggedPostId: string) {
|
||||
return Client4.getFlaggedPost(flaggedPostId);
|
||||
}
|
||||
|
||||
function getSearchContentReviewersFunction(teamId: string) {
|
||||
return (term: string) => {
|
||||
return Client4.searchContentFlaggingReviewers(term, teamId);
|
||||
|
|
@ -235,18 +205,6 @@ function saveReviewerSelection(flaggedPostId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function getChannel(flaggedPostId: string) {
|
||||
return (channelId: string) => {
|
||||
return Client4.getChannel(channelId, true, flaggedPostId);
|
||||
};
|
||||
}
|
||||
|
||||
function getTeam(flaggedPostId: string) {
|
||||
return (teamId: string) => {
|
||||
return Client4.getTeam(teamId, true, flaggedPostId);
|
||||
};
|
||||
}
|
||||
|
||||
function generateFileDownloadUrl(flaggedPostId: string) {
|
||||
return (fileId: string) => getFileDownloadUrl(fileId, true, flaggedPostId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ import PropertyValueRenderer from './propertyValueRenderer/propertyValueRenderer
|
|||
import './properties_card_view.scss';
|
||||
|
||||
export type PostPreviewFieldMetadata = {
|
||||
getPost?: (postId: string) => Promise<Post>;
|
||||
post?: Post;
|
||||
fetchDeletedPost?: boolean;
|
||||
getChannel?: (channelId: string) => Promise<Channel>;
|
||||
getTeam?: (teamId: string) => Promise<Team>;
|
||||
channel?: Channel;
|
||||
team?: Team;
|
||||
generateFileDownloadUrl?: (fileId: string) => string;
|
||||
};
|
||||
|
||||
|
|
@ -36,11 +36,11 @@ export type TextFieldMetadata = {
|
|||
};
|
||||
|
||||
export type ChannelFieldMetadata = {
|
||||
getChannel?: (channelId: string) => Promise<Channel>;
|
||||
channel?: Channel;
|
||||
};
|
||||
|
||||
export type TeamFieldMetadata = {
|
||||
getTeam?: (teamId: string) => Promise<Team>;
|
||||
team?: Team;
|
||||
};
|
||||
|
||||
export type FieldMetadata = PostPreviewFieldMetadata | TextFieldMetadata | UserPropertyMetadata | ChannelFieldMetadata | TeamFieldMetadata;
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ import {TestHelper} from 'utils/test_helper';
|
|||
|
||||
import ChannelPropertyRenderer from './channel_property_renderer';
|
||||
|
||||
jest.mock('components/common/hooks/useChannel');
|
||||
|
||||
const mockUseChannel = require('components/common/hooks/useChannel').useChannel as jest.MockedFunction<typeof import('components/common/hooks/useChannel').useChannel>;
|
||||
|
||||
describe('ChannelPropertyRenderer', () => {
|
||||
const mockChannel: Channel = {
|
||||
...TestHelper.getChannelMock({
|
||||
|
|
@ -30,34 +26,23 @@ describe('ChannelPropertyRenderer', () => {
|
|||
} as PropertyValue<string>;
|
||||
|
||||
it('should render channel name and icon when channel exists', () => {
|
||||
mockUseChannel.mockReturnValue(mockChannel);
|
||||
|
||||
renderWithContext(
|
||||
<ChannelPropertyRenderer value={mockValue}/>,
|
||||
<ChannelPropertyRenderer
|
||||
value={mockValue}
|
||||
metadata={{channel: mockChannel}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('channel-property')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Channel')).toBeInTheDocument();
|
||||
expect(mockUseChannel).toHaveBeenCalledWith('channel-id-123');
|
||||
});
|
||||
|
||||
it('should render deleted channel message when channel does not exist', () => {
|
||||
mockUseChannel.mockReturnValue(undefined);
|
||||
|
||||
renderWithContext(
|
||||
<ChannelPropertyRenderer value={mockValue}/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('channel-property')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Deleted channel ID: channel-id-123/)).toBeInTheDocument();
|
||||
expect(mockUseChannel).toHaveBeenCalledWith('channel-id-123');
|
||||
});
|
||||
|
||||
it('should render deleted channel message when channel is undefined', () => {
|
||||
mockUseChannel.mockReturnValue(undefined);
|
||||
|
||||
renderWithContext(
|
||||
<ChannelPropertyRenderer value={mockValue}/>,
|
||||
<ChannelPropertyRenderer
|
||||
value={mockValue}
|
||||
metadata={{channel: undefined}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('channel-property')).toBeInTheDocument();
|
||||
|
|
@ -70,10 +55,12 @@ describe('ChannelPropertyRenderer', () => {
|
|||
type: 'P' as const,
|
||||
display_name: 'Private Channel',
|
||||
};
|
||||
mockUseChannel.mockReturnValue(privateChannel);
|
||||
|
||||
renderWithContext(
|
||||
<ChannelPropertyRenderer value={mockValue}/>,
|
||||
<ChannelPropertyRenderer
|
||||
value={mockValue}
|
||||
metadata={{channel: privateChannel}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Private Channel')).toBeInTheDocument();
|
||||
|
|
@ -85,10 +72,12 @@ describe('ChannelPropertyRenderer', () => {
|
|||
type: 'D' as const,
|
||||
display_name: 'Direct Message',
|
||||
};
|
||||
mockUseChannel.mockReturnValue(dmChannel);
|
||||
|
||||
renderWithContext(
|
||||
<ChannelPropertyRenderer value={mockValue}/>,
|
||||
<ChannelPropertyRenderer
|
||||
value={mockValue}
|
||||
metadata={{channel: dmChannel}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Direct Message')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {FormattedMessage} from 'react-intl';
|
|||
|
||||
import type {PropertyValue} from '@mattermost/types/properties';
|
||||
|
||||
import {usePropertyCardViewChannelLoader} from 'components/common/hooks/usePropertyCardViewChannelLoader';
|
||||
import type {ChannelFieldMetadata} from 'components/properties_card_view/properties_card_view';
|
||||
import SidebarBaseChannelIcon from 'components/sidebar/sidebar_channel/sidebar_base_channel/sidebar_base_channel_icon';
|
||||
|
||||
|
|
@ -19,7 +18,7 @@ type Props = {
|
|||
|
||||
export default function ChannelPropertyRenderer({value, metadata}: Props) {
|
||||
const channelId = value.value as string;
|
||||
const channel = usePropertyCardViewChannelLoader(channelId, metadata?.getChannel);
|
||||
const channel = metadata?.channel;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, waitFor} from '@testing-library/react';
|
||||
import {waitFor} from '@testing-library/react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
|
@ -13,6 +14,8 @@ import type {DeepPartial} from '@mattermost/types/utilities';
|
|||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import type {PostPreviewFieldMetadata} from 'components/properties_card_view/properties_card_view';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
|
|
@ -20,16 +23,6 @@ import type {GlobalState} from 'types/store';
|
|||
|
||||
import PostPreviewPropertyRenderer from './post_preview_property_renderer';
|
||||
|
||||
jest.mock('components/common/hooks/useChannel');
|
||||
jest.mock('components/common/hooks/use_team');
|
||||
jest.mock('components/common/hooks/usePost');
|
||||
jest.mock('mattermost-redux/client');
|
||||
|
||||
const mockUseChannel = require('components/common/hooks/useChannel').useChannel as jest.MockedFunction<any>;
|
||||
const mockUseTeam = require('components/common/hooks/use_team').useTeam as jest.MockedFunction<any>;
|
||||
const mockedUsePost = require('components/common/hooks/usePost').usePost as jest.MockedFunction<any>;
|
||||
const mockedClient4 = jest.mocked(Client4);
|
||||
|
||||
describe('PostPreviewPropertyRenderer', () => {
|
||||
const mockUser: UserProfile = {
|
||||
...TestHelper.getUserMock(),
|
||||
|
|
@ -70,7 +63,10 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
metadata: {
|
||||
fetchDeletedPost: true,
|
||||
getPost: (postId: string) => Client4.getFlaggedPost(postId),
|
||||
},
|
||||
post: mockPost,
|
||||
channel: mockChannel,
|
||||
team: mockTeam,
|
||||
} as PostPreviewFieldMetadata,
|
||||
};
|
||||
|
||||
const baseState: DeepPartial<GlobalState> = {
|
||||
|
|
@ -101,15 +97,7 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedUsePost.mockReturnValue(null);
|
||||
mockedClient4.getFlaggedPost.mockResolvedValue(mockPost);
|
||||
});
|
||||
|
||||
it('should render PostMessagePreview when all data is available', async () => {
|
||||
mockUseChannel.mockReturnValue(mockChannel);
|
||||
mockUseTeam.mockReturnValue(mockTeam);
|
||||
|
||||
const {getByTestId, getByText} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
baseState,
|
||||
|
|
@ -124,42 +112,38 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
});
|
||||
|
||||
it('should return null when post is not found', async () => {
|
||||
mockUseChannel.mockReturnValue(mockChannel);
|
||||
mockUseTeam.mockReturnValue(mockTeam);
|
||||
mockedClient4.getFlaggedPost.mockRejectedValue({message: 'Post not found'});
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.post = undefined;
|
||||
|
||||
const {container} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
await act(async () => {});
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when channel is not found', async () => {
|
||||
mockUseChannel.mockReturnValue(null);
|
||||
mockUseTeam.mockReturnValue(mockTeam);
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.channel = undefined;
|
||||
|
||||
const {container} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when team is not found', async () => {
|
||||
mockUseChannel.mockReturnValue(mockChannel);
|
||||
mockUseTeam.mockReturnValue(null);
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.team = undefined;
|
||||
|
||||
const {container} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
|
|
@ -169,15 +153,14 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
type: 'P' as const,
|
||||
};
|
||||
|
||||
mockUseChannel.mockReturnValue(privateChannel);
|
||||
mockUseTeam.mockReturnValue(mockTeam);
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.channel = privateChannel;
|
||||
|
||||
const {getByTestId, getByText} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
expect(getByTestId('post-preview-property')).toBeVisible();
|
||||
expect(getByText('Test post message')).toBeVisible();
|
||||
expect(getByText('Originally posted in ~Test Channel')).toBeVisible();
|
||||
|
|
@ -194,15 +177,15 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
name: '',
|
||||
};
|
||||
|
||||
mockUseChannel.mockReturnValue(channelWithoutDisplayName);
|
||||
mockUseTeam.mockReturnValue(teamWithoutName);
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.channel = channelWithoutDisplayName;
|
||||
props.metadata.team = teamWithoutName;
|
||||
|
||||
const {getByTestId, getByText} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
expect(getByTestId('post-preview-property')).toBeVisible();
|
||||
expect(getByText('Test post message')).toBeVisible();
|
||||
expect(getByText('Originally posted in ~')).toBeVisible();
|
||||
|
|
@ -233,16 +216,14 @@ describe('PostPreviewPropertyRenderer', () => {
|
|||
},
|
||||
} as Post;
|
||||
|
||||
mockedClient4.getFlaggedPost.mockResolvedValue(postWithAttachments);
|
||||
mockUseChannel.mockReturnValue(mockChannel);
|
||||
mockUseTeam.mockReturnValue(mockTeam);
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.post = postWithAttachments;
|
||||
|
||||
const {getByTestId, getByText} = renderWithContext(
|
||||
<PostPreviewPropertyRenderer {...defaultProps}/>,
|
||||
<PostPreviewPropertyRenderer {...props}/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
expect(getByTestId('post-preview-property')).toBeVisible();
|
||||
expect(getByText('Post with file attachment')).toBeVisible();
|
||||
expect(getByText('Originally posted in ~Test Channel')).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -7,25 +7,20 @@ import {useIntl} from 'react-intl';
|
|||
import type {PostPreviewMetadata} from '@mattermost/types/posts';
|
||||
import type {PropertyValue} from '@mattermost/types/properties';
|
||||
|
||||
import {usePropertyCardViewChannelLoader} from 'components/common/hooks/usePropertyCardViewChannelLoader';
|
||||
import {usePropertyCardViewPostLoader} from 'components/common/hooks/usePropertyCardViewPostLoader';
|
||||
import {usePropertyCardViewTeamLoader} from 'components/common/hooks/usePropertyCardViewTeamLoader';
|
||||
import PostMessagePreview from 'components/post_view/post_message_preview';
|
||||
import type {PostPreviewFieldMetadata} from 'components/properties_card_view/properties_card_view';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
type Props = {
|
||||
value: PropertyValue<unknown>;
|
||||
value?: PropertyValue<unknown>;
|
||||
metadata?: PostPreviewFieldMetadata;
|
||||
}
|
||||
|
||||
export default function PostPreviewPropertyRenderer({value, metadata}: Props) {
|
||||
const postId = value.value as string;
|
||||
|
||||
const post = usePropertyCardViewPostLoader(postId, metadata?.getPost, true);
|
||||
const channel = usePropertyCardViewChannelLoader(post?.channel_id, metadata?.getChannel);
|
||||
const team = usePropertyCardViewTeamLoader(channel?.team_id, metadata?.getTeam);
|
||||
export default function PostPreviewPropertyRenderer({metadata}: Props) {
|
||||
const post = metadata?.post;
|
||||
const channel = metadata?.channel;
|
||||
const team = metadata?.team;
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import React from 'react';
|
||||
|
||||
import type {PropertyValue} from '@mattermost/types/properties';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import state from 'mattermost-redux/store/initial_state';
|
||||
|
||||
import type {TeamFieldMetadata} from 'components/properties_card_view/properties_card_view';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
|
|
@ -23,23 +28,14 @@ describe('TeamPropertyRenderer', () => {
|
|||
value: {
|
||||
value: 'team-id-123',
|
||||
} as PropertyValue<string>,
|
||||
metadata: {} as TeamFieldMetadata,
|
||||
};
|
||||
|
||||
test('should render team name and icon when team exists', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {
|
||||
'team-id-123': mockTeam,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = cloneDeep(defaultProps);
|
||||
props.metadata.team = mockTeam;
|
||||
|
||||
renderWithContext(
|
||||
<TeamPropertyRenderer {...defaultProps}/>,
|
||||
state,
|
||||
);
|
||||
renderWithContext(<TeamPropertyRenderer {...props}/>, state);
|
||||
|
||||
expect(screen.getByTestId('team-property')).toBeVisible();
|
||||
expect(screen.getByText('Test Team')).toBeVisible();
|
||||
|
|
@ -48,14 +44,6 @@ describe('TeamPropertyRenderer', () => {
|
|||
});
|
||||
|
||||
test('should render deleted team message when team does not exist', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<TeamPropertyRenderer {...defaultProps}/>,
|
||||
state,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
|||
|
||||
import type {PropertyValue} from '@mattermost/types/properties';
|
||||
|
||||
import {usePropertyCardViewTeamLoader} from 'components/common/hooks/usePropertyCardViewTeamLoader';
|
||||
import type {TeamFieldMetadata} from 'components/properties_card_view/properties_card_view';
|
||||
import {TeamIcon} from 'components/widgets/team_icon/team_icon';
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ type Props = {
|
|||
export default function TeamPropertyRenderer({value, metadata}: Props) {
|
||||
const intl = useIntl();
|
||||
const teamId = value.value as string;
|
||||
const team = usePropertyCardViewTeamLoader(teamId, metadata?.getTeam);
|
||||
const team = metadata?.team;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -8,4 +8,7 @@ export default keyMirror({
|
|||
RECEIVED_POST_CONTENT_FLAGGING_FIELDS: null,
|
||||
RECEIVED_POST_CONTENT_FLAGGING_VALUES: null,
|
||||
CONTENT_FLAGGING_REPORT_VALUE_UPDATED: null,
|
||||
RECEIVED_FLAGGED_POST: null,
|
||||
RECEIVED_CONTENT_FLAGGING_CHANNEL: null,
|
||||
RECEIVED_CONTENT_FLAGGING_TEAM: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {waitFor} from '@testing-library/react';
|
||||
import nock from 'nock';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import * as Actions from 'mattermost-redux/actions/content_flagging';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
|
|
@ -40,3 +43,127 @@ describe('Actions.getTeamContentFlaggingStatus', () => {
|
|||
expect(enabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.loadContentFlaggingTeam', () => {
|
||||
const store = configureStore();
|
||||
beforeAll(() => {
|
||||
TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('should dispatch RECEIVED_CONTENT_FLAGGING_TEAM on success', async () => {
|
||||
const mockTeam = {
|
||||
id: 'team_id',
|
||||
name: 'test-team',
|
||||
display_name: 'Test Team',
|
||||
};
|
||||
|
||||
nock(Client4.getTeamsRoute()).
|
||||
get('/team_id').
|
||||
query({
|
||||
flagged_post_id: 'post_id',
|
||||
as_content_reviewer: true,
|
||||
}).
|
||||
reply(200, mockTeam);
|
||||
|
||||
await store.dispatch(Actions.loadContentFlaggingTeam({teamId: 'team_id', flaggedPostId: 'post_id'}));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState() as GlobalState;
|
||||
const teams = state.entities.contentFlagging.teams;
|
||||
expect(teams?.team_id).toEqual(mockTeam);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not make API call when teamId or flaggedPostId is missing', async () => {
|
||||
await store.dispatch(Actions.loadContentFlaggingTeam({teamId: 'team_id'}));
|
||||
await store.dispatch(Actions.loadContentFlaggingTeam({flaggedPostId: 'post_id'}));
|
||||
|
||||
// Wait for the data loader to process
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
||||
// No API calls should have been made, so no new teams should be added
|
||||
const initialTeamCount = Object.keys(store.getState().entities.teams.teams).length;
|
||||
expect(initialTeamCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.loadContentFlaggingChannel', () => {
|
||||
const store = configureStore();
|
||||
beforeAll(() => {
|
||||
TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('should dispatch RECEIVED_CONTENT_FLAGGING_CHANNEL on success', async () => {
|
||||
const mockChannel = {
|
||||
id: 'channel_id',
|
||||
name: 'test-channel',
|
||||
display_name: 'Test Channel',
|
||||
team_id: 'team_id',
|
||||
};
|
||||
|
||||
nock(Client4.getChannelsRoute()).
|
||||
get('/channel_id').
|
||||
query({flagged_post_id: 'post_id', as_content_reviewer: true}).
|
||||
reply(200, mockChannel);
|
||||
|
||||
await store.dispatch(Actions.loadContentFlaggingChannel({channelId: 'channel_id', flaggedPostId: 'post_id'}));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState() as GlobalState;
|
||||
const channels = state.entities.contentFlagging.channels;
|
||||
expect(channels?.channel_id).toEqual(mockChannel);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not make API call when channelId or flaggedPostId is missing', async () => {
|
||||
await store.dispatch(Actions.loadContentFlaggingChannel({channelId: 'channel_id'}));
|
||||
await store.dispatch(Actions.loadContentFlaggingChannel({flaggedPostId: 'post_id'}));
|
||||
|
||||
// Wait for the data loader to process
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
||||
// No API calls should have been made, so no new channels should be added
|
||||
const initialChannelCount = Object.keys(store.getState().entities.channels.channels).length;
|
||||
expect(initialChannelCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.loadFlaggedPost', () => {
|
||||
const store = configureStore();
|
||||
beforeAll(() => {
|
||||
TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('should dispatch RECEIVED_FLAGGED_POST on success', async () => {
|
||||
const mockPost = {
|
||||
id: 'post_id',
|
||||
message: 'Test post message',
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
};
|
||||
|
||||
nock(Client4.getContentFlaggingRoute()).
|
||||
get('/post/post_id').
|
||||
reply(200, mockPost);
|
||||
|
||||
await store.dispatch(Actions.loadFlaggedPost('post_id'));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState() as GlobalState;
|
||||
const posts = state.entities.contentFlagging.flaggedPosts;
|
||||
expect(posts?.post_id).toEqual(mockPost);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {ContentFlaggingConfig} from '@mattermost/types/content_flagging';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {NameMappedPropertyFields, PropertyValue} from '@mattermost/types/properties';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {TeamTypes, ContentFlaggingTypes} from 'mattermost-redux/action_types';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
|
|
@ -11,6 +14,24 @@ import {Client4} from 'mattermost-redux/client';
|
|||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
import {DelayedDataLoader} from 'mattermost-redux/utils/data_loader';
|
||||
|
||||
export type ContentFlaggingChannelRequestIdentifier = {
|
||||
channelId?: string;
|
||||
flaggedPostId?: string;
|
||||
}
|
||||
|
||||
export type ContentFlaggingTeamRequestIdentifier = {
|
||||
teamId?: string;
|
||||
flaggedPostId?: string;
|
||||
}
|
||||
|
||||
function channelComparator(a: ContentFlaggingChannelRequestIdentifier, b: ContentFlaggingChannelRequestIdentifier) {
|
||||
return a.channelId === b.channelId;
|
||||
}
|
||||
|
||||
function teamComparator(a: ContentFlaggingTeamRequestIdentifier, b: ContentFlaggingTeamRequestIdentifier) {
|
||||
return a.teamId === b.teamId;
|
||||
}
|
||||
|
||||
export function getTeamContentFlaggingStatus(teamId: string): ActionFuncAsync<{enabled: boolean}> {
|
||||
return async (dispatch, getState) => {
|
||||
let response;
|
||||
|
|
@ -94,6 +115,132 @@ export function loadPostContentFlaggingFields(): ActionFuncAsync<NameMappedPrope
|
|||
};
|
||||
}
|
||||
|
||||
function getFlaggedPost(flaggedPostId: string): ActionFuncAsync<Post> {
|
||||
return async (dispatch, getState) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getFlaggedPost(flaggedPostId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
|
||||
data,
|
||||
});
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFlaggedPost(flaggedPostId: string): ActionFuncAsync<Post> {
|
||||
return async (dispatch, getState, {loaders}: any) => {
|
||||
if (!loaders.flaggedPostLoader) {
|
||||
loaders.flaggedPostLoader = new DelayedDataLoader<Post['id']>({
|
||||
fetchBatch: ([postId]) => dispatch(getFlaggedPost(postId)),
|
||||
maxBatchSize: 1,
|
||||
wait: 200,
|
||||
});
|
||||
}
|
||||
|
||||
const loader = loaders.flaggedPostLoader as DelayedDataLoader<Post['id']>;
|
||||
loader.queue([flaggedPostId]);
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
function getContentFlaggingChannel(channelId: string, flaggedPostId: string): ActionFuncAsync<Channel> {
|
||||
return async (dispatch, getState) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getChannel(channelId, true, flaggedPostId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_CHANNEL,
|
||||
data,
|
||||
});
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadContentFlaggingChannel(identifier: ContentFlaggingChannelRequestIdentifier): ActionFuncAsync<Channel> {
|
||||
return async (dispatch, getState, {loaders}: any) => {
|
||||
if (!loaders.contentFlaggingChannelLoader) {
|
||||
loaders.contentFlaggingChannelLoader =
|
||||
new DelayedDataLoader<ContentFlaggingChannelRequestIdentifier>({
|
||||
fetchBatch: ([{flaggedPostId, channelId}]) => {
|
||||
if (channelId && flaggedPostId) {
|
||||
return dispatch(getContentFlaggingChannel(channelId, flaggedPostId));
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
maxBatchSize: 1,
|
||||
wait: 200,
|
||||
comparator: channelComparator,
|
||||
});
|
||||
}
|
||||
|
||||
const loader = loaders.contentFlaggingChannelLoader as DelayedDataLoader<ContentFlaggingChannelRequestIdentifier>;
|
||||
loader.queue([identifier]);
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
function getContentFlaggingTeam(teamId: string, flaggedPostId: string): ActionFuncAsync<Team> {
|
||||
return async (dispatch, getState) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getTeam(teamId, true, flaggedPostId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_TEAM,
|
||||
data,
|
||||
});
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadContentFlaggingTeam(identifier: ContentFlaggingTeamRequestIdentifier): ActionFuncAsync<Team> {
|
||||
return async (dispatch, getState, {loaders}: any) => {
|
||||
if (!loaders.contentFlaggingTeamLoader) {
|
||||
loaders.contentFlaggingTeamLoader =
|
||||
new DelayedDataLoader<ContentFlaggingTeamRequestIdentifier>({
|
||||
fetchBatch: ([{flaggedPostId, teamId}]) => {
|
||||
if (teamId && flaggedPostId) {
|
||||
return dispatch(getContentFlaggingTeam(teamId, flaggedPostId));
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
maxBatchSize: 1,
|
||||
wait: 200,
|
||||
comparator: teamComparator,
|
||||
});
|
||||
}
|
||||
|
||||
const loader = loaders.contentFlaggingTeamLoader as DelayedDataLoader<ContentFlaggingTeamRequestIdentifier>;
|
||||
loader.queue([identifier]);
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostContentFlaggingValues(postId: string): ActionFuncAsync<Array<PropertyValue<unknown>>> {
|
||||
return async (dispatch, getState) => {
|
||||
let response;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import type {
|
|||
} from '@mattermost/types/properties';
|
||||
|
||||
import type {MMReduxAction} from 'mattermost-redux/action_types';
|
||||
import {ContentFlaggingTypes} from 'mattermost-redux/action_types';
|
||||
import {ContentFlaggingTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
function settings(state: ContentFlaggingState['settings'] = {} as ContentFlaggingConfig, action: MMReduxAction) {
|
||||
switch (action.type) {
|
||||
|
|
@ -23,6 +23,8 @@ function settings(state: ContentFlaggingState['settings'] = {} as ContentFlaggin
|
|||
...action.data,
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -36,6 +38,8 @@ function fields(state: ContentFlaggingState['fields'] = {} as NameMappedProperty
|
|||
...action.data,
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -67,6 +71,53 @@ function postValues(state: ContentFlaggingState['postValues'] = {}, action: MMRe
|
|||
[postId]: Object.values(valuesByFieldId),
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function flaggedPosts(state: ContentFlaggingState['flaggedPosts'] = {}, action: MMReduxAction) {
|
||||
switch (action.type) {
|
||||
case ContentFlaggingTypes.RECEIVED_FLAGGED_POST: {
|
||||
return {
|
||||
...state,
|
||||
[action.data.id]: action.data,
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function channels(state: ContentFlaggingState['channels'] = {}, action: MMReduxAction) {
|
||||
switch (action.type) {
|
||||
case ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_CHANNEL: {
|
||||
return {
|
||||
...state,
|
||||
[action.data.id]: action.data,
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function teams(state: ContentFlaggingState['teams'] = {}, action: MMReduxAction) {
|
||||
switch (action.type) {
|
||||
case ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_TEAM: {
|
||||
return {
|
||||
...state,
|
||||
[action.data.id]: action.data,
|
||||
};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -76,4 +127,7 @@ export default combineReducers({
|
|||
settings,
|
||||
fields,
|
||||
postValues,
|
||||
flaggedPosts,
|
||||
channels,
|
||||
teams,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {contentFlaggingFeatureEnabled} from './content_flagging';
|
||||
import {contentFlaggingFeatureEnabled, getContentFlaggingChannel, getContentFlaggingTeam, getFlaggedPost} from './content_flagging';
|
||||
|
||||
describe('Selectors.ContentFlagging', () => {
|
||||
test('should return true when config and feature flag both are set', () => {
|
||||
|
|
@ -50,3 +50,270 @@ describe('Selectors.ContentFlagging', () => {
|
|||
expect(contentFlaggingFeatureEnabled(state as GlobalState)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentFlaggingChannel', () => {
|
||||
test('should return undefined when channelId is not provided', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
channels: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingChannel(state as GlobalState, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return channel from regular channels store when available', () => {
|
||||
const mockChannel = {
|
||||
id: 'channel_id',
|
||||
name: 'test-channel',
|
||||
display_name: 'Test Channel',
|
||||
team_id: 'team_id',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
channel_id: mockChannel,
|
||||
},
|
||||
},
|
||||
contentFlagging: {
|
||||
channels: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingChannel(state as GlobalState, {channelId: 'channel_id'})).toEqual(mockChannel);
|
||||
});
|
||||
|
||||
test('should return channel from content flagging store when not in regular store', () => {
|
||||
const mockChannel = {
|
||||
id: 'channel_id',
|
||||
name: 'flagged-channel',
|
||||
display_name: 'Flagged Channel',
|
||||
team_id: 'team_id',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
channels: {
|
||||
channel_id: mockChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingChannel(state as GlobalState, {channelId: 'channel_id'})).toEqual(mockChannel);
|
||||
});
|
||||
|
||||
test('should prefer regular channels store over content flagging store', () => {
|
||||
const regularChannel = {
|
||||
id: 'channel_id',
|
||||
name: 'regular-channel',
|
||||
display_name: 'Regular Channel',
|
||||
team_id: 'team_id',
|
||||
};
|
||||
|
||||
const contentFlaggingChannel = {
|
||||
id: 'channel_id',
|
||||
name: 'flagged-channel',
|
||||
display_name: 'Flagged Channel',
|
||||
team_id: 'team_id',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
channel_id: regularChannel,
|
||||
},
|
||||
},
|
||||
contentFlagging: {
|
||||
channels: {
|
||||
channel_id: contentFlaggingChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingChannel(state as GlobalState, {channelId: 'channel_id'})).toEqual(regularChannel);
|
||||
});
|
||||
|
||||
test('should return undefined when channel does not exist in either store', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
channels: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingChannel(state as GlobalState, {channelId: 'nonexistent_channel'})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentFlaggingTeam', () => {
|
||||
test('should return undefined when teamId is not provided', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
teams: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingTeam(state as GlobalState, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return team from regular teams store when available', () => {
|
||||
const mockTeam = {
|
||||
id: 'team_id',
|
||||
name: 'test-team',
|
||||
display_name: 'Test Team',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {
|
||||
team_id: mockTeam,
|
||||
},
|
||||
},
|
||||
contentFlagging: {
|
||||
teams: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingTeam(state as GlobalState, {teamId: 'team_id'})).toEqual(mockTeam);
|
||||
});
|
||||
|
||||
test('should return team from content flagging store when not in regular store', () => {
|
||||
const mockTeam = {
|
||||
id: 'team_id',
|
||||
name: 'flagged-team',
|
||||
display_name: 'Flagged Team',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
teams: {
|
||||
team_id: mockTeam,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingTeam(state as GlobalState, {teamId: 'team_id'})).toEqual(mockTeam);
|
||||
});
|
||||
|
||||
test('should prefer regular teams store over content flagging store', () => {
|
||||
const regularTeam = {
|
||||
id: 'team_id',
|
||||
name: 'regular-team',
|
||||
display_name: 'Regular Team',
|
||||
};
|
||||
|
||||
const contentFlaggingTeam = {
|
||||
id: 'team_id',
|
||||
name: 'flagged-team',
|
||||
display_name: 'Flagged Team',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {
|
||||
team_id: regularTeam,
|
||||
},
|
||||
},
|
||||
contentFlagging: {
|
||||
teams: {
|
||||
team_id: contentFlaggingTeam,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingTeam(state as GlobalState, {teamId: 'team_id'})).toEqual(regularTeam);
|
||||
});
|
||||
|
||||
test('should return undefined when team does not exist in either store', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
teams: {},
|
||||
},
|
||||
contentFlagging: {
|
||||
teams: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getContentFlaggingTeam(state as GlobalState, {teamId: 'nonexistent_team'})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFlaggedPost', () => {
|
||||
test('should return flagged post when it exists', () => {
|
||||
const mockPost = {
|
||||
id: 'post_id',
|
||||
message: 'Test post message',
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
contentFlagging: {
|
||||
flaggedPosts: {
|
||||
post_id: mockPost,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getFlaggedPost(state as GlobalState, 'post_id')).toEqual(mockPost);
|
||||
});
|
||||
|
||||
test('should return undefined when flagged post does not exist', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
contentFlagging: {
|
||||
flaggedPosts: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getFlaggedPost(state as GlobalState, 'nonexistent_post')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when flaggedPosts is undefined', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
contentFlagging: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getFlaggedPost(state as GlobalState, 'post_id')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import type {ContentFlaggingChannelRequestIdentifier, ContentFlaggingTeamRequestIdentifier} from 'mattermost-redux/actions/content_flagging';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
export const contentFlaggingFeatureEnabled = (state: GlobalState): boolean => {
|
||||
|
|
@ -26,3 +27,34 @@ export const postContentFlaggingValues = (state: GlobalState, postId: string) =>
|
|||
const values = state.entities.contentFlagging.postValues || {};
|
||||
return values[postId];
|
||||
};
|
||||
|
||||
export const getFlaggedPost = (state: GlobalState, flaggedPostId: string) => {
|
||||
return state.entities.contentFlagging.flaggedPosts?.[flaggedPostId];
|
||||
};
|
||||
|
||||
export const getContentFlaggingChannel = (state: GlobalState, {channelId}: ContentFlaggingChannelRequestIdentifier) => {
|
||||
// Return channel from the regular channel store if available, else get it from the content flagging store
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const channel = state.entities.channels.channels[channelId];
|
||||
if (channel) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
return state.entities.contentFlagging.channels?.[channelId];
|
||||
};
|
||||
|
||||
export const getContentFlaggingTeam = (state: GlobalState, {teamId}: ContentFlaggingTeamRequestIdentifier) => {
|
||||
if (!teamId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const team = state.entities.teams.teams[teamId];
|
||||
if (team) {
|
||||
return team;
|
||||
}
|
||||
|
||||
return state.entities.contentFlagging.teams?.[teamId];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -160,6 +160,66 @@ describe('BackgroundDataLoader', () => {
|
|||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should dedupe object identifiers using custom comparator', () => {
|
||||
type ObjectIdentifier = {id: string; extra?: string};
|
||||
const fetchBatch = jest.fn();
|
||||
const comparator = (a: ObjectIdentifier, b: ObjectIdentifier) => a.id === b.id;
|
||||
|
||||
const objectLoader = new BackgroundDataLoader<ObjectIdentifier>({
|
||||
fetchBatch,
|
||||
maxBatchSize: 10,
|
||||
comparator,
|
||||
});
|
||||
|
||||
objectLoader.startIntervalIfNeeded(period);
|
||||
|
||||
// Queue objects with the same id but different references
|
||||
objectLoader.queue([{id: 'obj1', extra: 'a'}]);
|
||||
objectLoader.queue([{id: 'obj1', extra: 'b'}]); // Should be deduped
|
||||
objectLoader.queue([{id: 'obj2'}]);
|
||||
objectLoader.queue([{id: 'obj2', extra: 'c'}]); // Should be deduped
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchBatch).toHaveBeenCalledWith([
|
||||
{id: 'obj1', extra: 'a'},
|
||||
{id: 'obj2'},
|
||||
]);
|
||||
|
||||
objectLoader.stopInterval();
|
||||
});
|
||||
|
||||
test('should not dedupe different objects when using custom comparator', () => {
|
||||
type ObjectIdentifier = {id: string; type: string};
|
||||
const fetchBatch = jest.fn();
|
||||
const comparator = (a: ObjectIdentifier, b: ObjectIdentifier) => a.id === b.id && a.type === b.type;
|
||||
|
||||
const objectLoader = new BackgroundDataLoader<ObjectIdentifier>({
|
||||
fetchBatch,
|
||||
maxBatchSize: 10,
|
||||
comparator,
|
||||
});
|
||||
|
||||
objectLoader.startIntervalIfNeeded(period);
|
||||
|
||||
// Queue objects with different ids or types
|
||||
objectLoader.queue([{id: 'obj1', type: 'channel'}]);
|
||||
objectLoader.queue([{id: 'obj1', type: 'team'}]); // Different type, should not be deduped
|
||||
objectLoader.queue([{id: 'obj2', type: 'channel'}]);
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchBatch).toHaveBeenCalledWith([
|
||||
{id: 'obj1', type: 'channel'},
|
||||
{id: 'obj1', type: 'team'},
|
||||
{id: 'obj2', type: 'channel'},
|
||||
]);
|
||||
|
||||
objectLoader.stopInterval();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DelayedDataLoader', () => {
|
||||
|
|
@ -526,4 +586,85 @@ describe('DelayedDataLoader', () => {
|
|||
expect(fourthResolved).toBe(true);
|
||||
expect(fifthResolved).toBe(true);
|
||||
});
|
||||
|
||||
test('should dedupe object identifiers using custom comparator', () => {
|
||||
type ObjectIdentifier = {id: string; extra?: string};
|
||||
const fetchBatch = jest.fn(() => Promise.resolve());
|
||||
const comparator = (a: ObjectIdentifier, b: ObjectIdentifier) => a.id === b.id;
|
||||
|
||||
const objectLoader = new DelayedDataLoader<ObjectIdentifier>({
|
||||
fetchBatch,
|
||||
maxBatchSize: 10,
|
||||
wait,
|
||||
comparator,
|
||||
});
|
||||
|
||||
// Queue objects with the same id but different references
|
||||
objectLoader.queue([{id: 'obj1', extra: 'a'}]);
|
||||
objectLoader.queue([{id: 'obj1', extra: 'b'}]); // Should be deduped
|
||||
objectLoader.queue([{id: 'obj2'}]);
|
||||
objectLoader.queue([{id: 'obj2', extra: 'c'}]); // Should be deduped
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchBatch).toHaveBeenCalledWith([
|
||||
{id: 'obj1', extra: 'a'},
|
||||
{id: 'obj2'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should not dedupe different objects when using custom comparator', () => {
|
||||
type ObjectIdentifier = {id: string; type: string};
|
||||
const fetchBatch = jest.fn(() => Promise.resolve());
|
||||
const comparator = (a: ObjectIdentifier, b: ObjectIdentifier) => a.id === b.id && a.type === b.type;
|
||||
|
||||
const objectLoader = new DelayedDataLoader<ObjectIdentifier>({
|
||||
fetchBatch,
|
||||
maxBatchSize: 10,
|
||||
wait,
|
||||
comparator,
|
||||
});
|
||||
|
||||
// Queue objects with different ids or types
|
||||
objectLoader.queue([{id: 'obj1', type: 'channel'}]);
|
||||
objectLoader.queue([{id: 'obj1', type: 'team'}]); // Different type, should not be deduped
|
||||
objectLoader.queue([{id: 'obj2', type: 'channel'}]);
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchBatch).toHaveBeenCalledWith([
|
||||
{id: 'obj1', type: 'channel'},
|
||||
{id: 'obj1', type: 'team'},
|
||||
{id: 'obj2', type: 'channel'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should dedupe across multiple queue calls using custom comparator', () => {
|
||||
type ObjectIdentifier = {channelId: string; flaggedPostId?: string};
|
||||
const fetchBatch = jest.fn(() => Promise.resolve());
|
||||
const comparator = (a: ObjectIdentifier, b: ObjectIdentifier) => a.channelId === b.channelId;
|
||||
|
||||
const objectLoader = new DelayedDataLoader<ObjectIdentifier>({
|
||||
fetchBatch,
|
||||
maxBatchSize: 10,
|
||||
wait,
|
||||
comparator,
|
||||
});
|
||||
|
||||
// Simulate real-world usage pattern from content flagging
|
||||
objectLoader.queue([{channelId: 'ch1', flaggedPostId: 'post1'}]);
|
||||
objectLoader.queue([{channelId: 'ch1', flaggedPostId: 'post2'}]); // Same channel, should be deduped
|
||||
objectLoader.queue([{channelId: 'ch2', flaggedPostId: 'post3'}]);
|
||||
objectLoader.queue([{channelId: 'ch1'}]); // Same channel, should be deduped
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(fetchBatch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchBatch).toHaveBeenCalledWith([
|
||||
{channelId: 'ch1', flaggedPostId: 'post1'},
|
||||
{channelId: 'ch2', flaggedPostId: 'post3'},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type Comparator = (a: any, b: any) => boolean;
|
||||
|
||||
/**
|
||||
* A DataLoader is an object that can be used to batch requests for fetching objects from the server for performance
|
||||
* reasons.
|
||||
|
|
@ -8,15 +10,18 @@
|
|||
abstract class DataLoader<Identifier, Result = unknown> {
|
||||
protected readonly fetchBatch: (identifiers: Identifier[]) => Result;
|
||||
private readonly maxBatchSize: number;
|
||||
private readonly comparator?: Comparator;
|
||||
|
||||
protected readonly pendingIdentifiers = new Set<Identifier>();
|
||||
|
||||
constructor(args: {
|
||||
fetchBatch: (identifiers: Identifier[]) => Result;
|
||||
maxBatchSize: number;
|
||||
comparator?: Comparator;
|
||||
}) {
|
||||
this.fetchBatch = args.fetchBatch;
|
||||
this.maxBatchSize = args.maxBatchSize;
|
||||
this.comparator = args.comparator;
|
||||
}
|
||||
|
||||
public queue(identifiersToLoad: Identifier[]): void {
|
||||
|
|
@ -25,7 +30,22 @@ abstract class DataLoader<Identifier, Result = unknown> {
|
|||
continue;
|
||||
}
|
||||
|
||||
this.pendingIdentifiers.add(identifier);
|
||||
// If a custom comparator is provided, manually check for duplicates
|
||||
if (this.comparator) {
|
||||
let exists = false;
|
||||
for (const existing of this.pendingIdentifiers) {
|
||||
if (this.comparator(existing, identifier)) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
this.pendingIdentifiers.add(identifier);
|
||||
}
|
||||
} else {
|
||||
// Without a comparator, Set automatically handles uniqueness
|
||||
this.pendingIdentifiers.add(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,12 +57,12 @@ abstract class DataLoader<Identifier, Result = unknown> {
|
|||
protected prepareBatch(): {identifiers: Identifier[]; moreToLoad: boolean} {
|
||||
let nextBatch;
|
||||
|
||||
// Since we can only fetch a defined number of user statuses at a time, we need to batch the requests
|
||||
// Since we can only fetch a defined number of identifiers at a time, we need to batch the requests
|
||||
if (this.pendingIdentifiers.size >= this.maxBatchSize) {
|
||||
nextBatch = [];
|
||||
|
||||
// We use temp buffer here to store up until max buffer size
|
||||
// and clear out processed user ids
|
||||
// and clear out processed identifiers
|
||||
for (const identifier of this.pendingIdentifiers) {
|
||||
nextBatch.push(identifier);
|
||||
this.pendingIdentifiers.delete(identifier);
|
||||
|
|
@ -52,7 +72,7 @@ abstract class DataLoader<Identifier, Result = unknown> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// If we have less than max buffer size, we can directly fetch the statuses
|
||||
// If we have less than max buffer size, we can directly fetch the data
|
||||
nextBatch = Array.from(this.pendingIdentifiers);
|
||||
this.pendingIdentifiers.clear();
|
||||
}
|
||||
|
|
@ -136,6 +156,7 @@ export class DelayedDataLoader<Identifier> extends DataLoader<Identifier, Promis
|
|||
fetchBatch: (identifiers: Identifier[]) => Promise<unknown>;
|
||||
maxBatchSize: number;
|
||||
wait: number;
|
||||
comparator?: Comparator;
|
||||
}) {
|
||||
super(args);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Channel} from './channels';
|
||||
import type {Post} from './posts';
|
||||
import type {
|
||||
NameMappedPropertyFields,
|
||||
PropertyValue,
|
||||
} from './properties';
|
||||
import type {Team} from './teams';
|
||||
|
||||
export type ContentFlaggingEvent = 'flagged' | 'assigned' | 'removed' | 'dismissed';
|
||||
|
||||
|
|
@ -22,9 +24,10 @@ export type ContentFlaggingConfig = {
|
|||
export type ContentFlaggingState = {
|
||||
settings?: ContentFlaggingConfig;
|
||||
fields?: NameMappedPropertyFields;
|
||||
postValues?: {
|
||||
[key: Post['id']]: Array<PropertyValue<unknown>>;
|
||||
};
|
||||
postValues?: {[key: Post['id']]: Array<PropertyValue<unknown>>};
|
||||
flaggedPosts?: {[key: Post['id']]: Post};
|
||||
channels?: {[key: Channel['id']]: Channel};
|
||||
teams?: {[key: Team['id']]: Team};
|
||||
};
|
||||
|
||||
export enum ContentFlaggingStatus {
|
||||
|
|
|
|||
Loading…
Reference in a new issue