From 22d0e66fbeab05dbdd88811523e5ec8be5d4e0a4 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Wed, 27 Aug 2025 10:33:57 +0530 Subject: [PATCH] Data spillage card (#33646) * WIP * Added post flagging properties setup * Added tests * Removed error in app startup when content flaghging setup fails * Updated sync condition: * WIP * MOved to data migration * lint fix * CI * added new migration mocks * Used setup for tests * some comment * removed empty files * Added another property field * WIP * Updated test * WIP * Added card component * WIP * Displayed post preview * WIP * WIP * Added team property: * Adde post author field * displayed post creation time * WIP * Added user selector * refactored to use field sub types * migration post types * Added actions * Added isRHS prop * bvase finished * Created separate single select * Making common selector * lint fixes * i18n fixes * cleanup * fix: correct UserSelector mock import and props in test file Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) * fixed a test * Added tests * Added tests * test: mock PostPreviewPropertyRenderer component in test file * Fix tests * Fix tests * Stored version in system key * test: add initial test file for use_team hook * feat: add tests for useTeam hook based on usePost hook tests * test: add useChannel hook test file * feat: Add tests for useChannel hook * test: add tests for DataSpillageAction component * Handled dleted channel and team * test: add comprehensive tests for ChannelPropertyRenderer * Added ChannelPropertyRenderer tests * test: add empty test file for post preview property renderer * test: add comprehensive tests for PostPreviewPropertyRenderer * test: update PostPreviewPropertyRenderer tests to use toBeVisible and assert content * Added p[ost property renderer test * test: remove PostMessagePreview mock and define base state for rendering * Added p[ost property renderer test * test: add test case for post with file attachments * test: add assertions for file attachments visibility in post preview * Added post property renderer test * test: add empty test file for select property renderer * test: add comprehensive tests for SelectPropertyRenderer * Added base tests for select property renderer * Added base tests for select property renderer * test: add empty test file for team property renderer * test: add comprehensive tests for TeamPropertyRenderer * test: add assertion for TeamIcon rendering in TeamPropertyRenderer test * test: use toBeVisible instead of toBeInTheDocument in team property renderer tests * test: replace toBeInTheDocument with toBeVisible for team name assertion * Added TeamPropertyRenderer tests * test: add test file for text property renderer * test: add comprehensive tests for TextPropertyRenderer * Added TextPropertyRenderer tests * test: add empty test file for timestamp property renderer * test: add comprehensive tests for TimestampPropertyRenderer * test: verify timestamp rendering with actual date and time values * test: remove redundant test id visibility check in timestamp property renderer test * feat: Add base state and test cases for 12 and 24 hour time formats * Added TimestampPropertyRenderer tests * test: add empty test file for user property renderer * test: add comprehensive tests for UserPropertyRenderer * WIP * test: improve user property renderer test assertions * Added UserPropertyRenderer tests * test: add empty test file for propertyValueRenderer * test: add comprehensive tests for PropertyValueRenderer with mocked components * test: update text property rendering test assertion * feat: add PropertyValue type casting in test files * Added PropertyValueRenderer test * lint fix * fixed tests * refactor: Update ChannelNotificationsModal tests to remove snapshot testing and improve assertions * refactor: replace fireEvent with userEvent in channel notifications modal tests * Updated test to not use snapshots and use deep rendering * refactor: Update DotMenu tests to use renderWithContext and userEvent * Updating tests * Updating tests * Updating tests * lint fix * CI * removed unused snapshots * Updated text colot and removed hover color effect * Lint fixes * SCSS lint fix * fixed a test * Used useUser gook --------- Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) --- server/channels/app/migrations.go | 4 +- .../content_reviewers.test.tsx | 7 +- .../content_reviewers/content_reviewers.tsx | 9 +- .../team_reviewers_section.test.tsx | 64 - .../team_reviewers_section.tsx | 9 +- .../user_multiselector/user_multiselect.scss | 6 + .../user_multiselector/user_multiselector.tsx | 150 +- .../user_profile_option.tsx | 24 +- .../user_profile_pill.test.tsx | 18 +- .../user_multiselector/user_profile_pill.tsx | 49 +- .../__snapshots__/group_option.test.tsx.snap | 7 + .../channel_notifications_modal.test.tsx.snap | 3181 ----------------- .../channel_notifications_modal.test.tsx | 180 +- .../common/hooks/useChannel.test.ts | 207 ++ .../src/components/common/hooks/useChannel.ts | 15 + .../components/common/hooks/use_team.test.ts | 229 ++ .../src/components/common/hooks/use_team.ts | 15 + .../__snapshots__/dot_menu.test.tsx.snap | 607 ---- .../dot_menu_empty.test.tsx.snap | 7 - .../dot_menu_mobile.test.tsx.snap | 7 - .../src/components/dot_menu/dot_menu.test.tsx | 78 +- .../dot_menu/dot_menu_empty.test.tsx | 60 +- .../dot_menu/dot_menu_mobile.test.tsx | 60 +- .../post_markdown/post_markdown.test.tsx | 23 + .../post_markdown/post_markdown.tsx | 15 + .../data_spillage_actions.scss | 7 + .../data_spillage_actions.test.tsx | 19 + .../data_spillage_actions.tsx | 35 + .../data_spillage_report.scss | 33 + .../data_spillage_report.test.tsx | 153 + .../data_spillage_report.tsx | 424 +++ .../post_message_preview.test.tsx.snap | 189 +- .../post_message_preview/avatar/avatar.tsx | 67 + .../post_message_preview.test.tsx | 4 + .../post_message_preview.tsx | 59 +- .../post_message_view.test.tsx.snap | 5 + .../post_message_view.test.tsx | 4 + .../post_message_view/post_message_view.tsx | 1 + .../properties_card_view.scss | 49 + .../properties_card_view.tsx | 109 + .../channel_property_renderer.scss | 8 + .../channel_property_renderer.test.tsx | 100 + .../channel_property_renderer.tsx | 49 + .../post_preview_property_renderer.test.tsx | 274 ++ .../post_preview_property_renderer.tsx | 62 + .../propertyValueRenderer.test.tsx | 444 +++ .../propertyValueRenderer.tsx | 74 + .../property_value_renderer.scss | 8 + .../selectPropertyRenderer.scss | 11 + .../selectPropertyRenderer.test.tsx | 197 + .../selectPropertyRenderer.tsx | 59 + .../team_property_renderer.scss | 16 + .../team_property_renderer.test.tsx | 116 + .../team_property_renderer.tsx | 55 + .../textPropertyRenderer.test.tsx | 103 + .../textPropertyRenderer.tsx | 21 + .../timestamp_property_renderer.test.tsx | 171 + .../timestamp_property_renderer.tsx | 31 + .../selectable_user_property_renderer.scss | 60 + .../selectable_user_property_renderer.tsx | 38 + .../userPropertyRenderer.test.tsx | 129 + .../userPropertyRenderer.tsx | 52 + .../user_property_renderer.scss | 8 + .../start_trial_form_modal.test.tsx.snap | 7 + .../user_settings_notifications.test.tsx.snap | 21 + webapp/channels/src/i18n/en.json | 7 + .../mattermost-redux/src/utils/theme_utils.ts | 2 +- .../__snapshots__/post_type.test.tsx.snap | 1 + .../src/plugins/test/post_type.test.tsx | 4 + .../src/tests/react_testing_utils.tsx | 1 + webapp/channels/src/utils/constants.tsx | 18 + webapp/platform/types/src/posts.ts | 1 + webapp/platform/types/src/properties.ts | 13 +- 73 files changed, 4227 insertions(+), 4123 deletions(-) delete mode 100644 webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap create mode 100644 webapp/channels/src/components/common/hooks/useChannel.test.ts create mode 100644 webapp/channels/src/components/common/hooks/useChannel.ts create mode 100644 webapp/channels/src/components/common/hooks/use_team.test.ts create mode 100644 webapp/channels/src/components/common/hooks/use_team.ts delete mode 100644 webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap delete mode 100644 webapp/channels/src/components/dot_menu/__snapshots__/dot_menu_empty.test.tsx.snap delete mode 100644 webapp/channels/src/components/dot_menu/__snapshots__/dot_menu_mobile.test.tsx.snap create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_actions/data_spillage_actions.scss create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_actions/data_spillage_actions.test.tsx create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_actions/data_spillage_actions.tsx create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_report.scss create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_report.test.tsx create mode 100644 webapp/channels/src/components/post_view/data_spillage_report/data_spillage_report.tsx create mode 100644 webapp/channels/src/components/post_view/post_message_preview/avatar/avatar.tsx create mode 100644 webapp/channels/src/components/properties_card_view/properties_card_view.scss create mode 100644 webapp/channels/src/components/properties_card_view/properties_card_view.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/channel_property_renderer/channel_property_renderer.scss create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/channel_property_renderer/channel_property_renderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/channel_property_renderer/channel_property_renderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/post_preview_property_renderer/post_preview_property_renderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/post_preview_property_renderer/post_preview_property_renderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/propertyValueRenderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/propertyValueRenderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/property_value_renderer.scss create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/select_property_renderer/selectPropertyRenderer.scss create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/select_property_renderer/selectPropertyRenderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/select_property_renderer/selectPropertyRenderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/team_property_renderer/team_property_renderer.scss create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/team_property_renderer/team_property_renderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/team_property_renderer/team_property_renderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/text_property_renderer/textPropertyRenderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/text_property_renderer/textPropertyRenderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/timestamp_property_renderer/timestamp_property_renderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/timestamp_property_renderer/timestamp_property_renderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/user_property_renderer/selectable_user_property_renderer.scss create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/user_property_renderer/selectable_user_property_renderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/user_property_renderer/userPropertyRenderer.test.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/user_property_renderer/userPropertyRenderer.tsx create mode 100644 webapp/channels/src/components/properties_card_view/propertyValueRenderer/user_property_renderer/user_property_renderer.scss diff --git a/server/channels/app/migrations.go b/server/channels/app/migrations.go index c7d1aa78127..a23fa4a3cb4 100644 --- a/server/channels/app/migrations.go +++ b/server/channels/app/migrations.go @@ -640,7 +640,7 @@ func (s *Server) doSetupContentFlaggingProperties() error { contentFlaggingPropertyNameStatus: { GroupID: group.ID, Name: contentFlaggingPropertyNameStatus, - Type: model.PropertyFieldTypeText, + Type: model.PropertyFieldTypeSelect, }, contentFlaggingPropertyNameReportingUserID: { GroupID: group.ID, @@ -650,7 +650,7 @@ func (s *Server) doSetupContentFlaggingProperties() error { contentFlaggingPropertyNameReportingReason: { GroupID: group.ID, Name: contentFlaggingPropertyNameReportingReason, - Type: model.PropertyFieldTypeText, + Type: model.PropertyFieldTypeSelect, }, contentFlaggingPropertyNameReportingComment: { GroupID: group.ID, diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx index 8255018fa9d..dcbb7a06cda 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx @@ -14,15 +14,16 @@ import ContentFlaggingContentReviewers from './content_reviewers'; // Mock the UserMultiSelector component jest.mock('../../content_flagging/user_multiselector/user_multiselector', () => ({ - UserMultiSelector: ({id, initialValue, onChange}: {id: string; initialValue: string[]; onChange: (userIds: string[]) => void}) => ( + __esModule: true, + UserSelector: ({id, multiSelectInitialValue, multiSelectOnChange}: {id: string; multiSelectInitialValue: string[]; multiSelectOnChange: (userIds: string[]) => void}) => (
- {initialValue.join(',')} + {multiSelectInitialValue.join(',')}
), })); diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx index dbd4b4c1ebc..1f1354ad407 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx @@ -16,7 +16,7 @@ import { SectionHeader, } from 'components/admin_console/system_properties/controls'; -import {UserMultiSelector} from '../../content_flagging/user_multiselector/user_multiselector'; +import {UserSelector} from '../../content_flagging/user_multiselector/user_multiselector'; import type {SystemConsoleCustomSettingsComponentProps} from '../../schema_admin_settings'; import './content_reviewers.scss'; @@ -148,10 +148,11 @@ export default function ContentFlaggingContentReviewers(props: SystemConsoleCust
-
diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx index e8526879e18..a37ce332c6b 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx @@ -18,21 +18,6 @@ jest.mock('mattermost-redux/actions/teams', () => ({ searchTeams: jest.fn(), })); -jest.mock('../../user_multiselector/user_multiselector', () => ({ - UserMultiSelector: ({id, initialValue, onChange}: {id: string; initialValue: string[]; onChange: (ids: string[]) => void}) => ( -
- {`Selected: {${initialValue.join(', ')}`} - -
- ), -})); - -jest.mock('components/widgets/team_icon/team_icon', () => ({ - TeamIcon: ({content}: {content: string}) =>
{content}
, -})); - const mockSearchTeams = jest.mocked(searchTeams); describe('TeamReviewersSection', () => { @@ -265,40 +250,6 @@ describe('TeamReviewersSection', () => { }); }); - test('should handle reviewer selection changes', async () => { - const onChange = jest.fn(); - renderWithContext( - , - ); - - await waitFor(() => { - expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10}); - }); - - await waitFor(() => { - const teamNameCells = screen.getAllByTestId('teamName'); - expect(teamNameCells).toHaveLength(2); - expect(teamNameCells[0]).toBeVisible(); - expect(teamNameCells[0]).toHaveTextContent('Team One'); - - expect(teamNameCells[1]).toBeVisible(); - expect(teamNameCells[1]).toHaveTextContent('Team Two'); - }); - - const changeReviewersButton = screen.getAllByText('Change Reviewers')[0]; - fireEvent.click(changeReviewersButton); - - expect(onChange).toHaveBeenCalledWith({ - team1: { - Enabled: false, - ReviewerIds: ['user1', 'user2'], - }, - }); - }); - test('should display existing reviewer settings', async () => { const teamReviewersSetting = { team1: { @@ -354,17 +305,6 @@ describe('TeamReviewersSection', () => { }); test('should reset page to 0 when searching', async () => { - // mockSearchTeams. - // mockReturnValueOnce({ - // data: {teams: mockTeams, total_count: 20}, - // } as never). - // mockResolvedValueOnce({ - // data: {teams: mockTeams, total_count: 20}, - // } as never). - // mockResolvedValueOnce({ - // data: {teams: mockTeams, total_count: 5}, - // } as never); - mockSearchTeams.mockReturnValueOnce(async () => ({ data: {teams: mockTeams, total_count: 20}, })). @@ -480,10 +420,6 @@ describe('TeamReviewersSection', () => { expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10}); }); - await waitFor(() => { - expect(screen.getAllByText('Team One')).toHaveLength(4); - }); - const updatedToggle = screen.getAllByRole('button', {name: /enable or disable content reviewers for this team/i})[0]; // Second click - disable diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx index 57fe019a2d4..0ce5b572f67 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx @@ -16,7 +16,7 @@ import {TeamIcon} from 'components/widgets/team_icon/team_icon'; import * as Utils from 'utils/utils'; -import {UserMultiSelector} from '../../user_multiselector/user_multiselector'; +import {UserSelector} from '../../user_multiselector/user_multiselector'; import './team_reviewers_section.scss'; @@ -141,10 +141,11 @@ export default function TeamReviewers({teamReviewersSetting, onChange}: Props): ), reviewers: ( - ), enabled: ( diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss index 5b4ac977d0d..b67d23f9466 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss +++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss @@ -13,4 +13,10 @@ display: flex; gap: 8px; } + + &.singleSelect { + .UserMultiSelector__value-container { + display: flex; + } + } } diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx index 9ec451d7a47..d3a32e0ab65 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import React, {type ReactElement, useCallback, useEffect, useMemo, useRef} from 'react'; import {useIntl} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; -import type {MultiValue} from 'react-select'; +import type {MultiValue, SingleValue} from 'react-select'; import AsyncSelect from 'react-select/async'; import type {UserProfile} from '@mattermost/types/users'; @@ -16,9 +16,9 @@ import {getUsersByIDs} from 'mattermost-redux/selectors/entities/users'; import type {GlobalState} from 'types/store'; -import {UserProfilePill} from './user_profile_pill'; +import {MultiUserProfilePill, SingleUserProfilePill} from './user_profile_pill'; -import {UserOptionComponent} from '../../content_flagging/user_multiselector/user_profile_option'; +import {MultiUserOptionComponent, SingleUserOptionComponent} from '../../content_flagging/user_multiselector/user_profile_option'; import {LoadingIndicator} from '../../system_users/system_users_filters_popover/system_users_filter_team'; import './user_multiselect.scss'; @@ -29,29 +29,51 @@ export type AutocompleteOptionType = { raw?: T; } -type Props = { - id: string; - className?: string; - onChange: (selectedUserIds: string[]) => void; - initialValue?: string[]; - hasError?: boolean; +const BASE_SELECT_COMPONENTS = { + LoadingIndicator, + DropdownIndicator: () => null, + IndicatorSeparator: () => null, +}; + +type MultiSelectProps = { + multiSelectOnChange?: (selectedUserIds: string[]) => void; + multiSelectInitialValue?: string[]; } -export function UserMultiSelector({id, className, onChange, initialValue, hasError}: Props) { +type SingleSelectProps = { + singleSelectOnChange?: (selectedUserIds: string) => void; + singleSelectInitialValue?: string; +} + +type Props = MultiSelectProps & SingleSelectProps & { + id: string; + isMulti: boolean; + className?: string; + hasError?: boolean; + placeholder?: React.ReactNode; + showDropdownIndicator?: boolean; +}; + +export function UserSelector({id, isMulti, className, multiSelectOnChange, multiSelectInitialValue, singleSelectOnChange, singleSelectInitialValue, hasError, placeholder, showDropdownIndicator}: Props) { const dispatch = useDispatch(); const {formatMessage} = useIntl(); const initialDataLoaded = useRef(false); + const initialValue = useMemo(() => { + return isMulti ? multiSelectInitialValue : [singleSelectInitialValue || '']; + }, [isMulti, multiSelectInitialValue, singleSelectInitialValue]); + useEffect(() => { const fetchInitialData = async () => { - await dispatch(getMissingProfilesByIds(initialValue || [])); + const param = isMulti ? multiSelectInitialValue : [singleSelectInitialValue || '']; + await dispatch(getMissingProfilesByIds(param || [])); initialDataLoaded.current = true; }; - if (initialValue && !initialDataLoaded.current) { + if (Boolean(initialValue) && !initialDataLoaded.current) { fetchInitialData(); } - }, [dispatch, initialValue]); + }, [dispatch, initialValue, isMulti, multiSelectInitialValue, singleSelectInitialValue]); const initialUsers = useSelector((state: GlobalState) => getUsersByIDs(state, initialValue || [])); const selectInitialValue = initialUsers. @@ -64,7 +86,7 @@ export function UserMultiSelector({id, className, onChange, initialValue, hasErr const userLoadingMessage = useCallback(() => formatMessage({id: 'admin.userMultiSelector.loading', defaultMessage: 'Loading users'}), [formatMessage]); const noUsersMessage = useCallback(() => formatMessage({id: 'admin.userMultiSelector.noUsers', defaultMessage: 'No users found'}), [formatMessage]); - const placeholder = formatMessage({id: 'admin.userMultiSelector.placeholder', defaultMessage: 'Start typing to search for users...'}); + const defaultPlaceholder = formatMessage({id: 'admin.userMultiSelector.placeholder', defaultMessage: 'Start typing to search for users...'}); const searchUsers = useMemo(() => debounce(async (searchTerm: string, callback) => { try { @@ -89,35 +111,87 @@ export function UserMultiSelector({id, className, onChange, initialValue, hasErr } }, 200), [dispatch]); - function handleOnChange(value: MultiValue>) { + const multiSelectHandleOnChange = useCallback((value: MultiValue>) => { const selectedUserIds = value.map((option) => option.value); - onChange?.(selectedUserIds); + multiSelectOnChange?.(selectedUserIds); + }, [multiSelectOnChange]); + + const singleSelectHandleOnChange = useCallback((value: SingleValue>) => { + const selectedUserIds = value?.value || ''; + singleSelectOnChange?.(selectedUserIds); + }, [singleSelectOnChange]); + + const multiSelectComponents = useMemo(() => { + const componentObj = { + ...BASE_SELECT_COMPONENTS, + Option: MultiUserOptionComponent, + MultiValue: MultiUserProfilePill, + }; + + if (showDropdownIndicator) { + // @ts-expect-error doing this any other way runs into TypeScript nightmares due to very complex ReactSelect types + delete componentObj.DropdownIndicator; + } + + return componentObj; + }, [showDropdownIndicator]); + + const singleSelectComponents = useMemo(() => { + const componentObj = { + ...BASE_SELECT_COMPONENTS, + Option: SingleUserOptionComponent, + SingleValue: SingleUserProfilePill, + }; + + if (showDropdownIndicator) { + // @ts-expect-error doing this any other way runs into TypeScript nightmares due to very complex ReactSelect types + delete componentObj.DropdownIndicator; + } + + return componentObj; + }, [showDropdownIndicator]); + + const baseProps = useMemo(() => { + return { + id, + inputId: `${id}_input`, + classNamePrefix: 'UserMultiSelector', + className: classNames('Input Input__focus', className, {error: hasError}), + isClearable: false, + hideSelectedOptions: true, + cacheOptions: true, + placeholder: placeholder || defaultPlaceholder, + loadingMessage: userLoadingMessage, + noOptionsMessage: noUsersMessage, + loadOptions: searchUsers, + menuPortalTarget: document.body, + }; + }, [className, defaultPlaceholder, hasError, id, noUsersMessage, placeholder, searchUsers, userLoadingMessage]); + + const containerClassName = classNames('UserMultiSelector', {multiSelect: isMulti, singleSelect: !isMulti}); + + if (isMulti) { + return ( +
+ +
+ ); } return ( -
+
null, - IndicatorSeparator: () => null, - Option: UserOptionComponent, - MultiValue: UserProfilePill, - }} + {...baseProps} + isMulti={false} + onChange={singleSelectHandleOnChange} + value={selectInitialValue ? selectInitialValue[0] : null} + components={singleSelectComponents} />
); diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx index 83124747dae..2ce251270b7 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx @@ -17,7 +17,29 @@ import type {AutocompleteOptionType} from './user_multiselector'; import './user_profile_option.scss'; -export function UserOptionComponent(props: OptionProps, true>) { +export function MultiUserOptionComponent(props: OptionProps, true>) { + const {data, innerProps} = props; + + const userProfile = data.raw; + const userDisplayName = useSelector((state: GlobalState) => getDisplayNameByUser(state, userProfile)); + + return ( +
+ + + {userDisplayName} +
+ ); +} + +export function SingleUserOptionComponent(props: OptionProps, false>) { const {data, innerProps} = props; const userProfile = data.raw; diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx index 505888dcebb..650587396eb 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx @@ -10,7 +10,7 @@ import {fireEvent, renderWithContext} from 'tests/react_testing_utils'; import {TestHelper} from 'utils/test_helper'; import type {AutocompleteOptionType} from './user_multiselector'; -import {UserProfilePill} from './user_profile_pill'; +import {MultiUserProfilePill} from './user_profile_pill'; describe('components/admin_console/content_flagging/user_multiselector/UserProfilePill', () => { const baseProps = { @@ -70,7 +70,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi test('should render user profile pill with avatar and display name', () => { const {container} = renderWithContext( - , + , initialState, ); @@ -79,7 +79,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi test('should render with correct user display name', () => { const {container} = renderWithContext( - , + , initialState, ); @@ -90,7 +90,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi test('should render Avatar component with correct props', () => { const {container} = renderWithContext( - , + , initialState, ); @@ -100,7 +100,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi test('should render Remove component with close icon', () => { const {container} = renderWithContext( - , + , initialState, ); @@ -118,7 +118,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi }; const {container} = renderWithContext( - , + , initialState, ); @@ -143,7 +143,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi } as unknown as MultiValueProps, true>; const {container} = renderWithContext( - , + , initialState, ); @@ -165,7 +165,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi }; const {container} = renderWithContext( - , + , stateWithUsernameDisplay, ); @@ -175,7 +175,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi test('should apply correct CSS classes', () => { const {container} = renderWithContext( - , + , initialState, ); diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx index b0e3f5e6961..d6b058e9f71 100644 --- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx +++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx @@ -1,8 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {JSX} from 'react'; import React from 'react'; import {useSelector} from 'react-redux'; +import type {SingleValueProps} from 'react-select'; import type {MultiValueProps} from 'react-select/dist/declarations/src/components/MultiValue'; import type {UserProfile} from '@mattermost/types/users'; @@ -32,9 +34,39 @@ function Remove(props: any) { ); } -export function UserProfilePill(props: MultiValueProps, true>) { +export function MultiUserProfilePill(props: MultiValueProps, true>) { const {data, innerProps, selectProps, removeProps} = props; + return ( + + ); +} + +export function SingleUserProfilePill(props: SingleValueProps, false>) { + const {data, innerProps, selectProps} = props; + + return ( + + ); +} + +type Props = { + data: AutocompleteOptionType; + innerProps: JSX.IntrinsicElements['div']; + selectProps: unknown; + removeProps?: JSX.IntrinsicElements['div']; +} + +function BaseUserProfilePill({data, innerProps, selectProps, removeProps}: Props) { const userProfile = data.raw; const userDisplayName = useSelector((state: GlobalState) => getDisplayNameByUser(state, userProfile)); @@ -51,12 +83,15 @@ export function UserProfilePill(props: MultiValueProps + { + removeProps && + + }
); } diff --git a/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap b/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap index 5aebc108033..258367774a2 100644 --- a/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap +++ b/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap @@ -184,6 +184,13 @@ Object { "queryByTitle": [Function], "replaceStoreState": [Function], "rerender": [Function], + "store": Object { + "@@observable": [Function], + "dispatch": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, "unmount": [Function], "updateStoreState": [Function], } diff --git a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap deleted file mode 100644 index c8635b16c00..00000000000 --- a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap +++ /dev/null @@ -1,3181 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ChannelNotificationsModal should check the options in the desktop notifications 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -