MM-63276, MM-62707: CPA LDAP/SAML links and Duplicate field (#30772)
* Squashed commit of the following: commit 42ef1ff8c3ff881b0f21cb4de23a5964f0bb106b Merge: 4940da4326c049748b88Author: Mattermost Build <build@mattermost.com> Date: Fri Mar 14 21:15:22 2025 +0200 Merge branch 'master' into MM-62695 commit 4940da4326ff787dd43fc486f06be415257181ae Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 12:14:45 2025 -0700 manage error the same way property field and value do commit cd9ec590264ca3751a55fc0926318cbb6f46d471 Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 12:05:02 2025 -0700 sanitize and validate commit 2672e90b06331ff2e31a7807737cc1feff1c1beb Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 12:02:20 2025 -0700 use Len test method commit d101950d655c7d6fd7a668a15554cc52a1d42667 Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 11:59:12 2025 -0700 rearrange consts commit ed1b8f66fb08f7be38615ced74282380d5d680b7 Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 11:58:56 2025 -0700 rename CPASortOrder method commit d3bc303fa3c9694d2f6d1531186f4fbe69efb956 Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 11:54:24 2025 -0700 rename CustomProfileAttributes in method to CPA commit b2323d44a6c3f31ea7c798f8a88c62878d5d2cdf Author: Julien Tant <julien@craftyx.fr> Date: Fri Mar 14 11:54:02 2025 -0700 Add SAML and LDAP attr commit d411ae9da5a078cfbac60c5662bee27622ff31e5 Author: Julien Tant <julien@craftyx.fr> Date: Wed Mar 12 11:58:21 2025 -0700 i18n commit 27bc74c71462ae08d496104c908454ebc4f2def3 Author: Julien Tant <julien@craftyx.fr> Date: Wed Mar 12 11:11:23 2025 -0700 fix TestDeleteCPAField test commit 0d77071225d8575ace89cbe38c6c06fa94d7696b Author: Julien Tant <julien@craftyx.fr> Date: Wed Mar 12 10:28:49 2025 -0700 err->appErr commit fe87a68caa49c7264c20c32cd4f598ffd18d503f Author: Julien Tant <julien@craftyx.fr> Date: Wed Mar 12 10:22:13 2025 -0700 i18n commit 3b3ddf978fce66d9506416a10c03b21d6a12e9d0 Author: Julien Tant <julien@craftyx.fr> Date: Wed Mar 12 10:21:12 2025 -0700 tests commit 2f898bd53b832dea60dcdb960a71cd47709cddf8 Author: Julien Tant <julien@craftyx.fr> Date: Tue Mar 11 15:09:12 2025 -0700 add validation and tests commit a8c20841af6e82a6bf4f5dde6324b5e56c4d754e Author: Julien Tant <julien@craftyx.fr> Date: Tue Mar 11 13:45:53 2025 -0700 code style commit 5bb3868b2ce93b9eb6e9d0836c597fa1176fbbbd Merge: a4180d5d8f7c25de2cffAuthor: Mattermost Build <build@mattermost.com> Date: Tue Mar 11 22:24:06 2025 +0200 Merge branch 'master' into MM-62695 commit a4180d5d8ff5e23a7a0a73b08806d37289e076ce Author: Julien Tant <julien@craftyx.fr> Date: Tue Feb 25 11:53:54 2025 -0700 use CPAField commit 988177024ceebe73750ce48c40ea0a9ca6db75d6 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Tue Feb 25 10:52:47 2025 -0700 refactor: Move validateCustomProfileAttributesField to Validate method on CPAField struct commit 783e64472c7ef3c1e33ce9a94b0716e96a72b105 Author: Julien Tant <julien@craftyx.fr> Date: Tue Feb 25 10:52:45 2025 -0700 refactor: Improve custom profile attributes field handling and validation commit aee06af59c748e74e5dc7718c1529527ac8c052a Author: Julien Tant (aider) <julien@craftyx.fr> Date: Tue Feb 25 10:50:29 2025 -0700 test: Add test case for CPA field with empty attributes commit 7ab4455f9feb88826fcf6abb3b93a8a7bb53ed3e Author: Julien Tant <julien@craftyx.fr> Date: Tue Feb 25 10:50:28 2025 -0700 refactor: Cleanup whitespace and remove empty Attrs in custom profile attributes test commit dbdb47d75818d58739c3962d8624388214382611 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Tue Feb 25 10:48:08 2025 -0700 test: Add test case for property field with empty attributes commit 43872e06933b40f45caadbca7708eab283cebd8d Author: Julien Tant (aider) <julien@craftyx.fr> Date: Tue Feb 25 10:44:39 2025 -0700 test: Add tests for NewCPAFieldFromPropertyField and CPAFieldToPropertyField commit 335f6b5c8901b6ecdc1e9f031a81695ffc269029 Merge: 01f632db46e8ef26196cAuthor: Julien Tant <julien@craftyx.fr> Date: Tue Feb 25 10:05:43 2025 -0700 Merge branch 'master' into MM-62695 commit 01f632db46cf0eeeea0bac337b10c66fad815a24 Author: Julien Tant <julien@craftyx.fr> Date: Thu Feb 13 19:16:33 2025 -0700 removed unused i18n commit 0214d7eb947d3d8ecfde169cb07c210ecbfc9c92 Merge: db0e371ca268c11e9ecbAuthor: Julien Tant <julien@craftyx.fr> Date: Thu Feb 13 19:15:08 2025 -0700 Merge remote-tracking branch 'origin/master' into MM-62695 commit db0e371ca239b5286e58ad264c46a8891eafbe9b Author: Julien Tant <julien@craftyx.fr> Date: Thu Feb 13 19:05:58 2025 -0700 generic options commit 3cd62774a1b54e5e9da0c1c5b2626de4f49116ca Merge: d95e5d983841e0f97176Author: Julien Tant <julien@craftyx.fr> Date: Thu Feb 13 10:48:47 2025 -0700 Merge remote-tracking branch 'origin/master' into MM-62695 commit d95e5d9838b9b00bd4ef9ec066df03cfc52596d7 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 17:07:28 2025 -0700 style commit 2256076ed174b25a5decc2631ee6abaa9fa1a3c1 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 17:06:22 2025 -0700 refactor: Make color field optional in custom profile attributes commit 7382b8ecb3964083d24d879210aa3d137983b404 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 16:40:10 2025 -0700 fix api test commit a54c2d653f9a2e46d989181f9ef5ffd3dcbdb8c0 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 16:02:27 2025 -0700 lint commit 9d2e3f78f6ba836b599df7f3cdd4766bf47cd600 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 15:50:31 2025 -0700 use custom types rather than string commit 4624df52cbda3cb9b0f8fa60b7a3d4f5ecf1c845 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 15:40:52 2025 -0700 refactor: Use consistent "ValidateCPAField" in error messages for custom profile attributes commit 42716170b7158cc4d3ce06cfef36046be6290def Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 15:36:04 2025 -0700 fix casing in custom profilte attributes test commit bcca3d87c47efcf95fcea2d1b380ce8aee89f7d4 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 15:33:26 2025 -0700 refactor: Modify CustomProfileAttributesSelectOption to use lowercase JSON keys commit 861e12c1fcaedb69d331120bd4ae58757df1fae1 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 15:29:23 2025 -0700 i18n commit a44f6f40eb5babab5f72a49078bec110a4645ddb Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 15:21:08 2025 -0700 improve test commit b2f002016fb7438a4507f9cb1cc236a43fb71e27 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 15:10:00 2025 -0700 test: Add test case for preserving option IDs when patching select field commit aac155ef23866d7b85ef49483eec5b610b2a8bc0 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 15:04:18 2025 -0700 don't do validation in constructor commit a88c092768fb0c46b76e72d6e27f417d3f25ba86 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 15:00:10 2025 -0700 feat: Enhance ID validation and trimming in custom profile attributes commit b6fb5f274a97f9de4d80899a406b4397d2a4983e Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 14:57:24 2025 -0700 test: Add test cases for preserving IDs in custom profile attributes commit bbb7f3e2610f4ebc96c6043504710f359d48a733 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 14:56:15 2025 -0700 refactor: Update CustomProfileAttributesSelectOption constructor to prioritize ID parameter commit dbd1728a14fd708e60a3c1180b21a155221a032a Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 14:50:14 2025 -0700 feat: Add validation for custom profile attributes fields commit e87689571bde77c2881f50af537930be9355f815 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 14:44:15 2025 -0700 test: Add comprehensive test for NewCustomProfileAttributesSelectOptionFromMap commit a2292d44ff2cc496e36633440c3d0cbdb1930e45 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 14:43:14 2025 -0700 feat: Add support for lowercase and title case keys in custom profile attributes map commit 47ca1848767d597f8305c4c183a68be476c034b9 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 14:43:12 2025 -0700 fix: Update custom profile attributes map keys to use capitalized names commit e8de7dfc6d01c73064c10f65e0ecc86c4b2ae320 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 14:01:31 2025 -0700 feat: Add comprehensive test cases for custom profile attributes field validation commit e798a53170d0aedbe25916cdaef3a11ac4191fd5 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 13:56:25 2025 -0700 add default visibility commit e54ea2ba2f19bbcea06c9deed1a28923a2db5532 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 13:22:04 2025 -0700 fix tests commit db1839a6e9b7d6a86ae334fa7bcae5e3aa622295 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 13:18:51 2025 -0700 feat: Add index number to validation error messages in custom profile attributes commit 35e29a0dfd3fb3cc09b7a2ec9234791719224c01 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 13:18:49 2025 -0700 refactor: Add validation and creation methods for custom profile attributes commit eac47527d27607fef30e840175c6ddb855445467 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 12:49:00 2025 -0700 feat: Add validation to prevent empty custom profile attribute options commit 1baece68f945de3d8fe8d4fe9df0ebcb243ba5f7 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 12:48:36 2025 -0700 refactor: Rename NewCustomProfileAttributeSelectOption to NewCustomProfileAttributesSelectOption commit 43710b018ab102f74ecaf2339e29593ae5dd3f55 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 12:46:43 2025 -0700 refactor: Replace map[string]bool with map[string]struct{} for key existence check commit 3263b04478cf69533e298535f75a9d6f252e46de Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 12:46:06 2025 -0700 feat: Add IsValid method to validate CustomProfileAttributesSelectOptions commit 4531710f560fbc22ab76f6a67597e154fec3c896 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 12:46:04 2025 -0700 refactor: Fix typo in custom profile attributes select option function name commit c1c821275c574e15a0e03987098891e191e9aa55 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 12:42:50 2025 -0700 gofmt commit 328c898a3f0c50f8b0dd83aa37c84c099342a8a4 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 12:41:14 2025 -0700 refactor: Trim spaces from name and color in custom profile attribute select option constructor commit b924431499b1b1472f377fd6c8bc1e726e8857c3 Author: Julien Tant <julien@craftyx.fr> Date: Wed Feb 12 12:41:12 2025 -0700 feat: Add custom profile attributes model with validation and constants commit 463ad161c5141cc8994bab0278cdc1df54f4b4e3 Author: Julien Tant (aider) <julien@craftyx.fr> Date: Wed Feb 12 12:40:19 2025 -0700 test: Add unit tests for custom profile attributes select options * sort_order int temp * name, type menu, dot menu * values add/remove * disabled when deleted * - max length - full height - validate options - clear attrs.options when not select/multiselect * Revert "sort_order int temp" This reverts commit ed675983c21965d7a9534e6c7b4eb38b8a751002. * Revert "Squashed commit of the following:" This reverts commit faf8b01169f0c285b7c77333f601e3cc7a1f4f18. * field values test * test type menu * test table * fix dot menu item id * test delete modal * test dot menu * fix system_properties test * fix user_properties_utils tests * fix attr sort_order type * i18n * fix lint * fix test types * lint scss * wip * fix attrs json * ldap/saml links * duplicate * menuitemlink - blockable * user_properties_values tests * add blockable link test * user_properties_dot_menu test * types * lint styling * fix test * i18n * self-review changes: - enforce field limit for duplicate flow - useMemo dep - disable non-text field type options when ldap/saml syncing active
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ea217e2352
Коммит
79561c44c2
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, render, screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
|
||||
import BlockableLink from './blockable_link';
|
||||
|
||||
jest.mock('utils/browser_history', () => ({
|
||||
getHistory: jest.fn().mockReturnValue({
|
||||
push: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('components/admin_console/blockable_link/BlockableLink', () => {
|
||||
const defaultProps = {
|
||||
to: '/admin_console/test',
|
||||
blocked: false,
|
||||
actions: {
|
||||
deferNavigation: jest.fn(),
|
||||
},
|
||||
children: 'Link Text',
|
||||
};
|
||||
|
||||
test('should render properly', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlockableLink {...defaultProps}/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Link Text')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', '/admin_console/test');
|
||||
});
|
||||
|
||||
test('should navigate directly when not blocked', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlockableLink {...defaultProps}/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Link Text'));
|
||||
expect(defaultProps.actions.deferNavigation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should defer navigation when blocked', () => {
|
||||
const blockedProps = {
|
||||
...defaultProps,
|
||||
blocked: true,
|
||||
};
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlockableLink {...blockedProps}/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Link Text'));
|
||||
expect(blockedProps.actions.deferNavigation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call custom onClick handler if provided', () => {
|
||||
const onClickProps = {
|
||||
...defaultProps,
|
||||
onClick: jest.fn(),
|
||||
};
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlockableLink {...onClickProps}/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Link Text'));
|
||||
expect(onClickProps.onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should apply additional props correctly', () => {
|
||||
const customProps = {
|
||||
...defaultProps,
|
||||
className: 'custom-class',
|
||||
id: 'custom-id',
|
||||
'data-testid': 'custom-test-id',
|
||||
};
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlockableLink {...customProps}/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveClass('custom-class');
|
||||
expect(link).toHaveAttribute('id', 'custom-id');
|
||||
expect(link).toHaveAttribute('data-testid', 'custom-test-id');
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
|
||||
import type {UserPropertyField} from '@mattermost/types/properties';
|
||||
|
||||
@@ -30,19 +31,23 @@ describe('UserPropertyDotMenu', () => {
|
||||
|
||||
const updateField = jest.fn();
|
||||
const deleteField = jest.fn();
|
||||
const createField = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderComponent = (field: UserPropertyField = baseField) => {
|
||||
const renderComponent = (field: UserPropertyField = baseField, dotMenuProps?: Partial<ComponentProps<typeof DotMenu>>) => {
|
||||
return renderWithContext(
|
||||
(
|
||||
<div>
|
||||
<DotMenu
|
||||
field={field}
|
||||
canCreate={true}
|
||||
{...dotMenuProps}
|
||||
updateField={updateField}
|
||||
deleteField={deleteField}
|
||||
createField={createField}
|
||||
/>
|
||||
<ModalController/>
|
||||
</div>
|
||||
@@ -105,6 +110,51 @@ describe('UserPropertyDotMenu', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays LDAP and SAML link menu options', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Open the menu
|
||||
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${baseField.id}`);
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
// Verify both link options are shown
|
||||
expect(screen.getByText('Link property to AD/LDAP')).toBeInTheDocument();
|
||||
expect(screen.getByText('Link property to SAML')).toBeInTheDocument();
|
||||
|
||||
// TODO mock history and verify the link actions
|
||||
});
|
||||
|
||||
it('handles field duplication', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Open the menu
|
||||
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${baseField.id}`);
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
// Click the duplicate option
|
||||
fireEvent.click(screen.getByText(/Duplicate property/));
|
||||
|
||||
// Wait for createField to be called
|
||||
await waitFor(() => {
|
||||
// Verify createField was called with the correct parameters
|
||||
expect(createField).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: baseField.id,
|
||||
name: 'Test Field (copy)',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('hides field duplication when at field limit', async () => {
|
||||
renderComponent(undefined, {canCreate: false});
|
||||
|
||||
// Open the menu
|
||||
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${baseField.id}`);
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
// Verify duplicate option is not shown
|
||||
expect(screen.queryByText(/Duplicate property/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles field deletion with confirmation when field exists in DB', async () => {
|
||||
renderComponent();
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {CheckIcon, ChevronRightIcon, DotsHorizontalIcon, EyeOutlineIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import {CheckIcon, ChevronRightIcon, DotsHorizontalIcon, EyeOutlineIcon, SyncIcon, TrashCanOutlineIcon, ContentCopyIcon} from '@mattermost/compass-icons/components';
|
||||
import type {FieldVisibility, UserPropertyField} from '@mattermost/types/properties';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
@@ -12,9 +12,10 @@ import * as Menu from 'components/menu';
|
||||
import './user_properties_dot_menu.scss';
|
||||
import {useUserPropertyFieldDelete} from './user_properties_delete_modal';
|
||||
import {isCreatePending} from './user_properties_utils';
|
||||
|
||||
type Props = {
|
||||
field: UserPropertyField;
|
||||
canCreate: boolean;
|
||||
createField: (field: UserPropertyField) => void;
|
||||
updateField: (field: UserPropertyField) => void;
|
||||
deleteField: (id: string) => void;
|
||||
}
|
||||
@@ -23,11 +24,23 @@ const menuId = 'user-property-field_dotmenu';
|
||||
|
||||
const DotMenu = ({
|
||||
field,
|
||||
canCreate,
|
||||
createField,
|
||||
updateField,
|
||||
deleteField,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const {promptDelete} = useUserPropertyFieldDelete();
|
||||
|
||||
const handleDuplicate = () => {
|
||||
const name = formatMessage({
|
||||
id: 'admin.system_properties.user_properties.dotmenu.duplicate.name_copy',
|
||||
defaultMessage: '{fieldName} (copy)',
|
||||
}, {fieldName: field.name});
|
||||
|
||||
createField({...field, attrs: {...field.attrs}, name});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (isCreatePending(field)) {
|
||||
// skip prompt when field is pending creation
|
||||
@@ -161,18 +174,53 @@ const DotMenu = ({
|
||||
)}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
<Menu.LinkItem
|
||||
id={`${menuId}_link_ad-ldap`}
|
||||
to={`/admin_console/authentication/ldap#custom_profile_attribute-${field.name}`}
|
||||
leadingElement={<SyncIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.dotmenu.ad_ldap.link_property.label'
|
||||
defaultMessage={'Link property to AD/LDAP'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.LinkItem
|
||||
id={`${menuId}_link_ad-ldap`}
|
||||
to={`/admin_console/authentication/saml#custom_profile_attribute-${field.name}`}
|
||||
leadingElement={<SyncIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.dotmenu.saml.link_property.label'
|
||||
defaultMessage={'Link property to SAML'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{canCreate && (
|
||||
<Menu.Item
|
||||
id={`${menuId}_duplicate`}
|
||||
onClick={handleDuplicate}
|
||||
leadingElement={<ContentCopyIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.dotmenu.duplicate.label'
|
||||
defaultMessage={'Duplicate property'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Menu.Item
|
||||
id={`${menuId}_delete`}
|
||||
onClick={handleDelete}
|
||||
isDestructive={true}
|
||||
leadingElement={<TrashCanOutlineIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.dotmenu.delete.label'
|
||||
defaultMessage={'Delete property'}
|
||||
/>
|
||||
)}
|
||||
leadingElement={<TrashCanOutlineIcon size={18}/>}
|
||||
/>
|
||||
</Menu.Container>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('UserPropertiesTable', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const createField = jest.fn();
|
||||
const updateField = jest.fn();
|
||||
const deleteField = jest.fn();
|
||||
const reorderField = jest.fn();
|
||||
@@ -67,6 +68,8 @@ describe('UserPropertiesTable', () => {
|
||||
return renderWithContext(
|
||||
<UserPropertiesTable
|
||||
data={collection}
|
||||
canCreate={true}
|
||||
createField={createField}
|
||||
updateField={updateField}
|
||||
deleteField={deleteField}
|
||||
reorderField={reorderField}
|
||||
@@ -149,6 +152,8 @@ describe('UserPropertiesTable', () => {
|
||||
renderWithContext(
|
||||
<UserPropertiesTable
|
||||
data={collection}
|
||||
canCreate={true}
|
||||
createField={createField}
|
||||
updateField={updateField}
|
||||
deleteField={deleteField}
|
||||
reorderField={reorderField}
|
||||
|
||||
@@ -25,11 +25,8 @@ import UserPropertyValues from './user_properties_values';
|
||||
|
||||
import {AdminConsoleListTable} from '../list_table';
|
||||
|
||||
type Props = {
|
||||
data: UserPropertyFields;
|
||||
}
|
||||
|
||||
type FieldActions = {
|
||||
createField: (field: UserPropertyField) => void;
|
||||
updateField: (field: UserPropertyField) => void;
|
||||
deleteField: (id: string) => void;
|
||||
reorderField: (field: UserPropertyField, nextOrder: number) => void;
|
||||
@@ -39,6 +36,12 @@ export const useUserPropertiesTable = (): SectionHook => {
|
||||
const [userPropertyFields, readIO, pendingIO, itemOps] = useUserPropertyFields();
|
||||
const nonDeletedCount = Object.values(userPropertyFields.data).filter((f) => f.delete_at === 0).length;
|
||||
|
||||
const canCreate = nonDeletedCount < Constants.MAX_CUSTOM_ATTRIBUTES;
|
||||
|
||||
const create = () => {
|
||||
itemOps.create();
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const newData = await pendingIO.commit();
|
||||
|
||||
@@ -54,12 +57,14 @@ export const useUserPropertiesTable = (): SectionHook => {
|
||||
<>
|
||||
<UserPropertiesTable
|
||||
data={userPropertyFields}
|
||||
canCreate={canCreate}
|
||||
createField={itemOps.create}
|
||||
updateField={itemOps.update}
|
||||
deleteField={itemOps.delete}
|
||||
reorderField={itemOps.reorder}
|
||||
/>
|
||||
{nonDeletedCount < Constants.MAX_CUSTOM_ATTRIBUTES && (
|
||||
<LinkButton onClick={itemOps.create}>
|
||||
{canCreate && (
|
||||
<LinkButton onClick={create}>
|
||||
<PlusIcon size={16}/>
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.add_property'
|
||||
@@ -82,7 +87,19 @@ export const useUserPropertiesTable = (): SectionHook => {
|
||||
};
|
||||
};
|
||||
|
||||
export function UserPropertiesTable({data: collection, updateField, deleteField, reorderField}: Props & FieldActions) {
|
||||
type Props = {
|
||||
data: UserPropertyFields;
|
||||
canCreate: boolean;
|
||||
}
|
||||
|
||||
export function UserPropertiesTable({
|
||||
data: collection,
|
||||
canCreate,
|
||||
createField,
|
||||
updateField,
|
||||
deleteField,
|
||||
reorderField,
|
||||
}: Props & FieldActions) {
|
||||
const {formatMessage} = useIntl();
|
||||
const data = collectionToArray(collection);
|
||||
const col = createColumnHelper<UserPropertyField>();
|
||||
@@ -216,6 +233,8 @@ export function UserPropertiesTable({data: collection, updateField, deleteField,
|
||||
<ActionsRoot>
|
||||
<DotMenu
|
||||
field={row.original}
|
||||
canCreate={canCreate}
|
||||
createField={createField}
|
||||
updateField={updateField}
|
||||
deleteField={deleteField}
|
||||
/>
|
||||
@@ -225,7 +244,7 @@ export function UserPropertiesTable({data: collection, updateField, deleteField,
|
||||
enableSorting: false,
|
||||
}),
|
||||
];
|
||||
}, [updateField, deleteField, collection.warnings]);
|
||||
}, [createField, updateField, deleteField, collection.warnings, canCreate]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
|
||||
@@ -96,6 +96,34 @@ describe('UserPropertyTypeMenu', () => {
|
||||
expect(screen.getAllByRole('menuitemradio')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('disables non-supported options when ldap-linked', () => {
|
||||
renderComponent({...baseField, attrs: {...baseField.attrs, ldap: 'ldapPropName'}});
|
||||
|
||||
// Open the menu
|
||||
fireEvent.click(screen.getByText('Text'));
|
||||
|
||||
// Non-text should be disabled
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Phone'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'URL'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Multi-select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('disables non-supported options when saml-linked', () => {
|
||||
renderComponent({...baseField, attrs: {...baseField.attrs, saml: 'samlPropName'}});
|
||||
|
||||
// Open the menu
|
||||
fireEvent.click(screen.getByText('Text'));
|
||||
|
||||
// Non-text should be disabled
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Phone'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'URL'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Multi-select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('menuitemradio', {name: 'Select'})).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('shows check icon for current type', () => {
|
||||
const selectField = {
|
||||
...baseField,
|
||||
|
||||
@@ -70,7 +70,7 @@ const SelectType = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
{[
|
||||
<Menu.Input
|
||||
<Menu.InputItem
|
||||
key='filter_types'
|
||||
id='filter_types'
|
||||
type='text'
|
||||
@@ -82,18 +82,22 @@ const SelectType = (props: Props) => {
|
||||
/>,
|
||||
]}
|
||||
{options.map((descriptor) => {
|
||||
const {id, icon: Icon, label, disabled} = descriptor;
|
||||
const {id, icon: Icon, label, hidden, canSync} = descriptor;
|
||||
|
||||
if (disabled) {
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSyncing = props.field.attrs.ldap || props.field.attrs.saml;
|
||||
const disabled = Boolean(isSyncing && !canSync);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={id}
|
||||
key={id}
|
||||
role='menuitemradio'
|
||||
forceCloseOnSelect={true}
|
||||
disabled={disabled}
|
||||
aria-checked={id === currentTypeDescriptor.id}
|
||||
onClick={() => handleTypeChange(descriptor)}
|
||||
labels={<FormattedMessage {...label}/>}
|
||||
@@ -131,7 +135,9 @@ type TypeDescriptor = {
|
||||
valueType: FieldValueType;
|
||||
icon: ComponentType<IconProps>;
|
||||
label: MessageDescriptor;
|
||||
disabled?: boolean;
|
||||
|
||||
hidden?: boolean;
|
||||
canSync?: boolean; // ldap/saml
|
||||
};
|
||||
|
||||
const TYPE_DESCRIPTOR: IDMappedObjects<TypeDescriptor> = {
|
||||
@@ -144,10 +150,11 @@ const TYPE_DESCRIPTOR: IDMappedObjects<TypeDescriptor> = {
|
||||
id: 'admin.system_properties.user_properties.table.select_type.text',
|
||||
defaultMessage: 'Text',
|
||||
}),
|
||||
canSync: true,
|
||||
},
|
||||
email: {
|
||||
id: 'email',
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
fieldType: 'text',
|
||||
valueType: 'email',
|
||||
icon: EmailOutlineIcon,
|
||||
|
||||
@@ -191,11 +191,22 @@ export const useUserPropertyFields = () => {
|
||||
return collectionReplaceItem(pending, field);
|
||||
});
|
||||
},
|
||||
create: () => {
|
||||
create: (patch?) => {
|
||||
pendingIO.apply((pending) => {
|
||||
const nextOrder = Object.values(pending.data).filter((x) => !isDeletePending(x)).length;
|
||||
const name = getIncrementedName('Text', pending);
|
||||
const field = newPendingField({name, type: 'text', attrs: {sort_order: nextOrder, visibility: 'when_set', value_type: ''}});
|
||||
|
||||
const field = newPendingField({
|
||||
type: 'text',
|
||||
...patch,
|
||||
name: getIncrementedName(patch?.name ?? 'Text', pending),
|
||||
attrs: {
|
||||
visibility: 'when_set',
|
||||
value_type: '',
|
||||
...patch?.attrs,
|
||||
sort_order: nextOrder,
|
||||
},
|
||||
});
|
||||
|
||||
return collectionAddItem(pending, field);
|
||||
});
|
||||
},
|
||||
@@ -268,9 +279,20 @@ export const isDeletePending = <T extends {delete_at: number; create_at: number}
|
||||
export const newPendingId = () => `${PENDING}${generateId()}`;
|
||||
|
||||
export const newPendingField = (patch: UserPropertyFieldPatch & Pick<UserPropertyField, 'name'>): UserPropertyField => {
|
||||
const attrs = {...patch.attrs};
|
||||
|
||||
if (attrs.options) {
|
||||
// clear option ids
|
||||
attrs.options = patch.attrs?.options?.map((option) => ({...option, id: ''}));
|
||||
}
|
||||
|
||||
// clear ldap/saml links
|
||||
Reflect.deleteProperty(attrs, 'ldap');
|
||||
Reflect.deleteProperty(attrs, 'saml');
|
||||
|
||||
return {
|
||||
...patch,
|
||||
type: 'text',
|
||||
...patch,
|
||||
group_id: 'custom_profile_attributes' satisfies UserPropertyFieldGroupID,
|
||||
id: newPendingId(),
|
||||
create_at: 0,
|
||||
@@ -280,8 +302,7 @@ export const newPendingField = (patch: UserPropertyFieldPatch & Pick<UserPropert
|
||||
visibility: 'when_set' satisfies FieldVisibility,
|
||||
sort_order: 0,
|
||||
value_type: '' satisfies FieldValueType,
|
||||
...patch.attrs,
|
||||
...attrs,
|
||||
},
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
.user-property-field-dotmenu-menu-button {
|
||||
height: 40px;
|
||||
justify-content: start;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
font-weight: normal;
|
||||
.user-property-field-values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-property-field-values__chip-link {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: var(--center-channel-color);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04)
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&[aria-expanded="true"] {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
color: #D24B4E;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&.strong {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,4 +143,77 @@ describe('UserPropertyValues', () => {
|
||||
const option = screen.getByText('Option 1');
|
||||
expect(option.closest('div[aria-disabled]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows LDAP sync information when field has LDAP attribute', () => {
|
||||
const ldapField = {
|
||||
...baseField,
|
||||
attrs: {
|
||||
...baseField.attrs,
|
||||
ldap: 'ldapAttribute',
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(ldapField);
|
||||
|
||||
// Check that the sync info is displayed
|
||||
expect(screen.getByText(/Synced with:/)).toBeInTheDocument();
|
||||
const ldapLink = screen.getByText('AD/LDAP: ldapAttribute');
|
||||
expect(ldapLink).toBeInTheDocument();
|
||||
|
||||
// Check that the link points to the correct location
|
||||
const linkElement = screen.getByTestId(`user-property-field-values__ldap-${ldapField.name}`);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(linkElement).toHaveAttribute('href', `/admin_console/authentication/ldap#custom_profile_attribute-${baseField.name}`);
|
||||
});
|
||||
|
||||
it('shows SAML sync information when field has SAML attribute', () => {
|
||||
const samlField = {
|
||||
...baseField,
|
||||
attrs: {
|
||||
...baseField.attrs,
|
||||
saml: 'samlAttribute',
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(samlField);
|
||||
|
||||
// Check that the sync info is displayed
|
||||
expect(screen.getByText(/Synced with:/)).toBeInTheDocument();
|
||||
const samlLink = screen.getByText('SAML: samlAttribute');
|
||||
expect(samlLink).toBeInTheDocument();
|
||||
|
||||
// Check that the link points to the correct location
|
||||
const linkElement = screen.getByTestId(`user-property-field-values__saml-${samlField.name}`);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(linkElement).toHaveAttribute('href', `/admin_console/authentication/saml#custom_profile_attribute-${baseField.name}`);
|
||||
});
|
||||
|
||||
it('shows both LDAP and SAML sync information when field has both attributes', () => {
|
||||
const syncedField = {
|
||||
...baseField,
|
||||
attrs: {
|
||||
...baseField.attrs,
|
||||
ldap: 'ldapAttribute',
|
||||
saml: 'samlAttribute',
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(syncedField);
|
||||
|
||||
// Check that the sync info is displayed
|
||||
expect(screen.getByText(/Synced with:/)).toBeInTheDocument();
|
||||
|
||||
const ldapLink = screen.getByText('AD/LDAP: ldapAttribute');
|
||||
expect(ldapLink).toBeInTheDocument();
|
||||
|
||||
const samlLink = screen.getByText('SAML: samlAttribute');
|
||||
expect(samlLink).toBeInTheDocument();
|
||||
|
||||
// Check that both links point to the correct locations
|
||||
const ldapLinkElement = screen.getByTestId(`user-property-field-values__ldap-${baseField.name}`);
|
||||
expect(ldapLinkElement).toBeInTheDocument();
|
||||
|
||||
const samlLinkElement = screen.getByTestId(`user-property-field-values__ldap-${baseField.name}`);
|
||||
expect(samlLinkElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
|
||||
import type {FocusEventHandler, KeyboardEventHandler} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {FormattedList, FormattedMessage, useIntl} from 'react-intl';
|
||||
import type {GroupBase} from 'react-select';
|
||||
import {components} from 'react-select';
|
||||
import type {CreatableProps} from 'react-select/creatable';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
|
||||
import {SyncIcon} from '@mattermost/compass-icons/components';
|
||||
import type {PropertyFieldOption, UserPropertyField} from '@mattermost/types/properties';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import {DangerText} from './controls';
|
||||
|
||||
// import './user_properties_dot_menu.scss';
|
||||
import './user_properties_values.scss';
|
||||
import BlockableLink from '../blockable_link';
|
||||
|
||||
type Props = {
|
||||
field: UserPropertyField;
|
||||
@@ -74,6 +76,52 @@ const UserPropertyValues = ({
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
if (field.attrs.ldap || field.attrs.saml) {
|
||||
const syncedProperties = [
|
||||
|
||||
field.attrs.ldap && (
|
||||
<BlockableLink
|
||||
className='user-property-field-values__chip-link'
|
||||
to={`/admin_console/authentication/ldap#custom_profile_attribute-${field.name}`}
|
||||
key={`${field.name}-ldap`}
|
||||
data-testid={`user-property-field-values__ldap-${field.name}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.table.values.synced_with.ldap'
|
||||
defaultMessage='AD/LDAP: {propertyName}'
|
||||
values={{propertyName: field.attrs.ldap}}
|
||||
/>
|
||||
</BlockableLink>
|
||||
),
|
||||
field.attrs.saml && (
|
||||
<BlockableLink
|
||||
className='user-property-field-values__chip-link'
|
||||
to={`/admin_console/authentication/saml#custom_profile_attribute-${field.name}`}
|
||||
key={`${field.name}-saml`}
|
||||
data-testid={`user-property-field-values__saml-${field.name}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.table.values.synced_with.saml'
|
||||
defaultMessage='SAML: {propertyName}'
|
||||
values={{propertyName: field.attrs.saml}}
|
||||
/>
|
||||
</BlockableLink>
|
||||
),
|
||||
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<span className='user-property-field-values'>
|
||||
<SyncIcon size={18}/>
|
||||
<FormattedMessage
|
||||
id='admin.system_properties.user_properties.table.values.synced_with'
|
||||
defaultMessage='Synced with: {syncedProperties}'
|
||||
values={{syncedProperties: <FormattedList value={syncedProperties}/>}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type !== 'multiselect' && field.type !== 'select') {
|
||||
return (
|
||||
<>
|
||||
@@ -138,6 +186,7 @@ const styles: SelectProps['styles'] = {
|
||||
paddingLeft: '6px',
|
||||
paddingTop: '1px',
|
||||
paddingBottom: '1px',
|
||||
backgroundColor: 'rgba(var(--center-channel-color-rgb), 0.08)',
|
||||
}),
|
||||
multiValueLabel: (base) => ({
|
||||
...base,
|
||||
|
||||
@@ -6,7 +6,8 @@ import './menu.scss';
|
||||
export {Menu as Container} from './menu';
|
||||
export {SubMenu} from './sub_menu';
|
||||
export {MenuItem as Item} from './menu_item';
|
||||
export {MenuItemInput as Input} from './menu_item_input';
|
||||
export {MenuItemInput as InputItem} from './menu_item_input';
|
||||
export {MenuItemLink as LinkItem} from './menu_item_link';
|
||||
export {MenuTitle as Title} from './menu_title';
|
||||
export type {FirstMenuItemProps} from './menu_item';
|
||||
export {MenuItemSeparator as Separator} from './menu_item_separator';
|
||||
|
||||
52
webapp/channels/src/components/menu/menu_item_link.tsx
Обычный файл
52
webapp/channels/src/components/menu/menu_item_link.tsx
Обычный файл
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import {useHistory, useLocation} from 'react-router-dom';
|
||||
|
||||
import {deferNavigation} from 'actions/admin_actions';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {Props as MenuItemProps} from './menu_item';
|
||||
import {MenuItem} from './menu_item';
|
||||
|
||||
import {getNavigationBlocked} from '../../selectors/views/admin';
|
||||
|
||||
type Props = MenuItemProps & {
|
||||
to: string;
|
||||
onClick?: MenuItemProps['onClick'];
|
||||
}
|
||||
|
||||
export function MenuItemLink({
|
||||
to,
|
||||
onClick,
|
||||
...otherProps
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const {pathname} = useLocation();
|
||||
|
||||
const blocked = useSelector((state: GlobalState) => pathname.startsWith('/admin_console') && getNavigationBlocked(state));
|
||||
|
||||
const handleClick: MenuItemProps['onClick'] = useCallback((e) => {
|
||||
onClick?.(e);
|
||||
|
||||
if (blocked) {
|
||||
e.preventDefault();
|
||||
dispatch(deferNavigation(() => {
|
||||
history.push(to);
|
||||
}));
|
||||
} else {
|
||||
history.push(to);
|
||||
}
|
||||
}, [blocked, onClick, deferNavigation, history.push, to]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={handleClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ const SelectTeam = (props: Props) => {
|
||||
// MUI Menu doesn't support fragments, and the recommended alternative is to use an array.
|
||||
const renderFilterArea = () => {
|
||||
const elements = [
|
||||
<Menu.Input
|
||||
<Menu.InputItem
|
||||
key='filter_teams'
|
||||
id='search_teams'
|
||||
type='text'
|
||||
|
||||
@@ -2592,7 +2592,11 @@
|
||||
"admin.system_properties.details.saving_changes": "Saving configuration…",
|
||||
"admin.system_properties.details.saving_changes_error": "There was an error while saving the configuration",
|
||||
"admin.system_properties.user_properties.add_property": "Add property",
|
||||
"admin.system_properties.user_properties.dotmenu.ad_ldap.link_property.label": "Link property to AD/LDAP",
|
||||
"admin.system_properties.user_properties.dotmenu.delete.label": "Delete property",
|
||||
"admin.system_properties.user_properties.dotmenu.duplicate.label": "Duplicate property",
|
||||
"admin.system_properties.user_properties.dotmenu.duplicate.name_copy": "{fieldName} (copy)",
|
||||
"admin.system_properties.user_properties.dotmenu.saml.link_property.label": "Link property to SAML",
|
||||
"admin.system_properties.user_properties.dotmenu.visibility.always.label": "Always show",
|
||||
"admin.system_properties.user_properties.dotmenu.visibility.hidden.label": "Always hide",
|
||||
"admin.system_properties.user_properties.dotmenu.visibility.label": "Visibility",
|
||||
@@ -2615,6 +2619,9 @@
|
||||
"admin.system_properties.user_properties.table.validation.values_unique": "Values must be unique.",
|
||||
"admin.system_properties.user_properties.table.values": "Values",
|
||||
"admin.system_properties.user_properties.table.values.placeholder": "Add values… (required)",
|
||||
"admin.system_properties.user_properties.table.values.synced_with": "Synced with: {syncedProperties}",
|
||||
"admin.system_properties.user_properties.table.values.synced_with.ldap": "AD/LDAP: {propertyName}",
|
||||
"admin.system_properties.user_properties.table.values.synced_with.saml": "SAML: {propertyName}",
|
||||
"admin.system_properties.user_properties.title": "User Properties",
|
||||
"admin.system_roles_feature_discovery.copy": "Assign customizable admin roles to give designated users read and/or write access to select sections of System Console.",
|
||||
"admin.system_roles_feature_discovery.title": "Provide controlled access to the System Console with Mattermost Enterprise",
|
||||
|
||||
Ссылка в новой задаче
Block a user