MM-63907, MM-63906: Smooth navigation between User Properties and LDAP/SAML pages. (#31127)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
2358699d91
Коммит
db27d1edec
@@ -1,7 +1,9 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import type {Location} from 'history';
|
||||||
|
import type {RefCallback} from 'react';
|
||||||
|
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {Route, Switch, Redirect} from 'react-router-dom';
|
import {Route, Switch, Redirect} from 'react-router-dom';
|
||||||
import type {RouteComponentProps} from 'react-router-dom';
|
import type {RouteComponentProps} from 'react-router-dom';
|
||||||
|
|
||||||
@@ -31,10 +33,6 @@ import type {PropsFromRedux} from './index';
|
|||||||
|
|
||||||
export type Props = PropsFromRedux & RouteComponentProps;
|
export type Props = PropsFromRedux & RouteComponentProps;
|
||||||
|
|
||||||
type State = {
|
|
||||||
search: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not every page in the system console will need the license and config, but the vast majority will
|
// not every page in the system console will need the license and config, but the vast majority will
|
||||||
type ExtraProps = {
|
type ExtraProps = {
|
||||||
enterpriseReady: boolean;
|
enterpriseReady: boolean;
|
||||||
@@ -49,39 +47,72 @@ type ExtraProps = {
|
|||||||
isCurrentUserSystemAdmin: boolean;
|
isCurrentUserSystemAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdminConsole extends React.PureComponent<Props, State> {
|
/**
|
||||||
public constructor(props: Props) {
|
* Focus or scroll to a provided hash for the given {@link Location}.
|
||||||
super(props);
|
* @returns a ref callback that should to be attached to an ancestor of the target hash element.
|
||||||
this.state = {
|
* @remarks emulates standard browser URL hash scroll-to behavior, but also works in custom or nested scroll containers.
|
||||||
search: '',
|
*/
|
||||||
};
|
const useFocusScroller = (location: Location): RefCallback<HTMLElement> => {
|
||||||
}
|
const lastFocusedLocation = useRef<Location>();
|
||||||
|
|
||||||
public componentDidMount(): void {
|
return useCallback((node) => {
|
||||||
this.props.actions.getConfig();
|
if (!node || !location.hash || lastFocusedLocation.current === location) {
|
||||||
this.props.actions.getEnvironmentConfig();
|
// if there is no node or hash, or if we've already focused the hash for this location
|
||||||
this.props.actions.loadRolesIfNeeded(['channel_user', 'team_user', 'system_user', 'channel_admin', 'team_admin', 'system_admin', 'system_user_manager', 'system_custom_group_admin', 'system_read_only_admin', 'system_manager']);
|
return;
|
||||||
this.props.actions.selectLhsItem(LhsItemType.None);
|
}
|
||||||
this.props.actions.selectTeam('');
|
|
||||||
|
const id = decodeURIComponent(location.hash.substring(1));
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// focus the element, or scroll it into view if it couldn't be focused as a fallback
|
||||||
|
element.focus();
|
||||||
|
if (document.activeElement !== element) {
|
||||||
|
element.scrollIntoView({behavior: 'auto'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// only focus a hash for a given location once
|
||||||
|
lastFocusedLocation.current = location;
|
||||||
|
}, [location]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminConsole = (props: Props) => {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const handleFocusScroller = useFocusScroller(props.location);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.actions.getConfig();
|
||||||
|
props.actions.getEnvironmentConfig();
|
||||||
|
props.actions.loadRolesIfNeeded(['channel_user', 'team_user', 'system_user', 'channel_admin', 'team_admin', 'system_admin', 'system_user_manager', 'system_custom_group_admin', 'system_read_only_admin', 'system_manager']);
|
||||||
|
props.actions.selectLhsItem(LhsItemType.None);
|
||||||
|
props.actions.selectTeam('');
|
||||||
document.body.classList.add('console__body');
|
document.body.classList.add('console__body');
|
||||||
document.getElementById('root')?.classList.add('console__root');
|
document.getElementById('root')?.classList.add('console__root');
|
||||||
resetTheme();
|
resetTheme();
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
return () => {
|
||||||
document.body.classList.remove('console__body');
|
document.body.classList.remove('console__body');
|
||||||
document.getElementById('root')?.classList.remove('console__root');
|
document.getElementById('root')?.classList.remove('console__root');
|
||||||
applyTheme(this.props.currentTheme);
|
applyTheme(props.currentTheme);
|
||||||
|
|
||||||
// Reset the admin console users management table properties
|
// Reset the admin console users management table properties
|
||||||
this.props.actions.setAdminConsoleUsersManagementTableProperties();
|
props.actions.setAdminConsoleUsersManagementTableProperties();
|
||||||
}
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
private handleSearchChange = (search: string) => {
|
const handleSearchChange = (searchTerm: string) => {
|
||||||
this.setState({search});
|
setSearch(searchTerm);
|
||||||
};
|
};
|
||||||
|
|
||||||
private mainRolesLoaded(roles: Record<string, Role>) {
|
const mainRolesLoaded = (roles: Record<string, Role>) => {
|
||||||
return (
|
return (
|
||||||
roles &&
|
roles &&
|
||||||
roles.channel_admin &&
|
roles.channel_admin &&
|
||||||
@@ -95,15 +126,15 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
|||||||
roles.system_custom_group_admin &&
|
roles.system_custom_group_admin &&
|
||||||
roles.system_manager
|
roles.system_manager
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
private renderRoutes = (extraProps: ExtraProps) => {
|
const renderRoutes = (extraProps: ExtraProps) => {
|
||||||
const {adminDefinition, config, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin} = this.props;
|
const {adminDefinition, config, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin} = props;
|
||||||
|
|
||||||
const schemas: AdminDefinitionSubSection[] = Object.values(adminDefinition).flatMap((section: AdminDefinitionSection) => {
|
const schemas: AdminDefinitionSubSection[] = Object.values(adminDefinition).flatMap((section: AdminDefinitionSection) => {
|
||||||
let isSectionHidden = false;
|
let isSectionHidden = false;
|
||||||
if (typeof section.isHidden === 'function') {
|
if (typeof section.isHidden === 'function') {
|
||||||
isSectionHidden = section.isHidden(config, this.state, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin);
|
isSectionHidden = section.isHidden(config, {search}, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin);
|
||||||
} else {
|
} else {
|
||||||
isSectionHidden = Boolean(section.isHidden);
|
isSectionHidden = Boolean(section.isHidden);
|
||||||
}
|
}
|
||||||
@@ -117,7 +148,7 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
const schemaRoutes = schemas.map((item: AdminDefinitionSubSection, index: number) => {
|
const schemaRoutes = schemas.map((item: AdminDefinitionSubSection, index: number) => {
|
||||||
if (typeof item.isHidden !== 'undefined') {
|
if (typeof item.isHidden !== 'undefined') {
|
||||||
const isHidden = (typeof item.isHidden === 'function') ? item.isHidden(config, this.state, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin) : Boolean(item.isHidden);
|
const isHidden = (typeof item.isHidden === 'function') ? item.isHidden(config, {search}, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin) : Boolean(item.isHidden);
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -126,7 +157,7 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
|||||||
let isItemDisabled: boolean;
|
let isItemDisabled: boolean;
|
||||||
|
|
||||||
if (typeof item.isDisabled === 'function') {
|
if (typeof item.isDisabled === 'function') {
|
||||||
isItemDisabled = item.isDisabled(config, this.state, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin);
|
isItemDisabled = item.isDisabled(config, {search}, license, buildEnterpriseReady, consoleAccess, cloud, isCurrentUserSystemAdmin);
|
||||||
} else {
|
} else {
|
||||||
isItemDisabled = Boolean(item.isDisabled);
|
isItemDisabled = Boolean(item.isDisabled);
|
||||||
}
|
}
|
||||||
@@ -144,12 +175,12 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={item.url}
|
key={item.url}
|
||||||
path={`${this.props.match.url}/${item.url}`}
|
path={`${props.match.url}/${item.url}`}
|
||||||
render={(props) => (
|
render={(routeProps) => (
|
||||||
<SchemaAdminSettings
|
<SchemaAdminSettings
|
||||||
{...extraProps}
|
{...extraProps}
|
||||||
{...props}
|
{...routeProps}
|
||||||
consoleAccess={this.props.consoleAccess}
|
consoleAccess={props.consoleAccess}
|
||||||
schema={item.schema}
|
schema={item.schema}
|
||||||
isDisabled={isItemDisabled}
|
isDisabled={isItemDisabled}
|
||||||
/>
|
/>
|
||||||
@@ -161,80 +192,79 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
{schemaRoutes}
|
{schemaRoutes}
|
||||||
{<Redirect to={`${this.props.match.url}/${defaultUrl}`}/>}
|
{<Redirect to={`${props.match.url}/${defaultUrl}`}/>}
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element | null {
|
const {
|
||||||
const {
|
license,
|
||||||
license,
|
config,
|
||||||
config,
|
environmentConfig,
|
||||||
environmentConfig,
|
showNavigationPrompt,
|
||||||
showNavigationPrompt,
|
roles,
|
||||||
roles,
|
} = props;
|
||||||
} = this.props;
|
const {setNavigationBlocked, cancelNavigation, confirmNavigation, editRole, patchConfig} = props.actions;
|
||||||
const {setNavigationBlocked, cancelNavigation, confirmNavigation, editRole, patchConfig} = this.props.actions;
|
|
||||||
|
|
||||||
if (!this.props.currentUserHasAnAdminRole) {
|
|
||||||
return (
|
|
||||||
<Redirect to={this.props.unauthorizedRoute}/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.mainRolesLoaded(this.props.roles)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(config).length === 0) {
|
|
||||||
return <div/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config && Object.keys(config).length === 0 && config.constructor === Object) {
|
|
||||||
return (
|
|
||||||
<div className='admin-console__wrapper admin-console'/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const extraProps: ExtraProps = {
|
|
||||||
enterpriseReady: this.props.buildEnterpriseReady,
|
|
||||||
license,
|
|
||||||
config,
|
|
||||||
environmentConfig,
|
|
||||||
setNavigationBlocked,
|
|
||||||
roles,
|
|
||||||
editRole,
|
|
||||||
patchConfig,
|
|
||||||
cloud: this.props.cloud,
|
|
||||||
isCurrentUserSystemAdmin: this.props.isCurrentUserSystemAdmin,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (!props.currentUserHasAnAdminRole) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Redirect to={props.unauthorizedRoute}/>
|
||||||
<AnnouncementBarController/>
|
|
||||||
<SystemNotice/>
|
|
||||||
<BackstageNavbar team={this.props.team}/>
|
|
||||||
<AdminSidebar onSearchChange={this.handleSearchChange}/>
|
|
||||||
<div
|
|
||||||
className='admin-console__wrapper admin-console'
|
|
||||||
id='adminConsoleWrapper'
|
|
||||||
>
|
|
||||||
<SearchKeywordMarking
|
|
||||||
keyword={this.state.search}
|
|
||||||
pathname={this.props.location.pathname}
|
|
||||||
>
|
|
||||||
{this.renderRoutes(extraProps)}
|
|
||||||
</SearchKeywordMarking>
|
|
||||||
</div>
|
|
||||||
<DiscardChangesModal
|
|
||||||
show={showNavigationPrompt}
|
|
||||||
onConfirm={confirmNavigation}
|
|
||||||
onCancel={cancelNavigation}
|
|
||||||
/>
|
|
||||||
<ModalController/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!mainRolesLoaded(props.roles)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(config).length === 0) {
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config && Object.keys(config).length === 0 && config.constructor === Object) {
|
||||||
|
return (
|
||||||
|
<div className='admin-console__wrapper admin-console'/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraProps: ExtraProps = {
|
||||||
|
enterpriseReady: props.buildEnterpriseReady,
|
||||||
|
license,
|
||||||
|
config,
|
||||||
|
environmentConfig,
|
||||||
|
setNavigationBlocked,
|
||||||
|
roles,
|
||||||
|
editRole,
|
||||||
|
patchConfig,
|
||||||
|
cloud: props.cloud,
|
||||||
|
isCurrentUserSystemAdmin: props.isCurrentUserSystemAdmin,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnnouncementBarController/>
|
||||||
|
<SystemNotice/>
|
||||||
|
<BackstageNavbar team={props.team}/>
|
||||||
|
<AdminSidebar onSearchChange={handleSearchChange}/>
|
||||||
|
<div
|
||||||
|
className='admin-console__wrapper admin-console'
|
||||||
|
id='adminConsoleWrapper'
|
||||||
|
ref={handleFocusScroller}
|
||||||
|
>
|
||||||
|
<SearchKeywordMarking
|
||||||
|
keyword={search}
|
||||||
|
pathname={props.location.pathname}
|
||||||
|
>
|
||||||
|
{renderRoutes(extraProps)}
|
||||||
|
</SearchKeywordMarking>
|
||||||
|
</div>
|
||||||
|
<DiscardChangesModal
|
||||||
|
show={showNavigationPrompt}
|
||||||
|
onConfirm={confirmNavigation}
|
||||||
|
onCancel={cancelNavigation}
|
||||||
|
/>
|
||||||
|
<ModalController/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminConsole;
|
export default AdminConsole;
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
|||||||
this.props.actions.getPlugins();
|
this.props.actions.getPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.searchRef.current) {
|
if (this.searchRef.current && !getHistory().location.hash) {
|
||||||
|
// default focus if no other target/hash is specified for auto-focus
|
||||||
this.searchRef.current.focus();
|
this.searchRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,13 +209,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
|||||||
definitionKey={subDefinitionKey}
|
definitionKey={subDefinitionKey}
|
||||||
name={item.url}
|
name={item.url}
|
||||||
restrictedIndicator={item.restrictedIndicator?.shouldDisplay(license, subscriptionProduct) ? item.restrictedIndicator.value(cloud) : undefined}
|
restrictedIndicator={item.restrictedIndicator?.shouldDisplay(license, subscriptionProduct) ? item.restrictedIndicator.value(cloud) : undefined}
|
||||||
title={
|
title={typeof item.title === 'string' ? item.title : (
|
||||||
typeof item.title === 'string' ?
|
<FormattedMessage
|
||||||
item.title :
|
{...item.title}
|
||||||
<FormattedMessage
|
/>
|
||||||
{...item.title}
|
)}
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@@ -237,13 +236,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
|||||||
parentLink='/admin_console'
|
parentLink='/admin_console'
|
||||||
icon={section.icon}
|
icon={section.icon}
|
||||||
sectionClass=''
|
sectionClass=''
|
||||||
title={
|
title={typeof section.sectionTitle === 'string' ? section.sectionTitle : (
|
||||||
typeof section.sectionTitle === 'string' ?
|
<FormattedMessage
|
||||||
section.sectionTitle :
|
{...section.sectionTitle}
|
||||||
<FormattedMessage
|
/>
|
||||||
{...section.sectionTitle}
|
)}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{sidebarItems}
|
{sidebarItems}
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
|
|||||||
@@ -30,3 +30,13 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-property-field-dotmenu__chip {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ describe('UserPropertyDotMenu', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays LDAP and SAML link menu options', async () => {
|
it('displays LDAP and SAML link menu options for existing fields', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
// Open the menu
|
// Open the menu
|
||||||
@@ -120,8 +120,63 @@ describe('UserPropertyDotMenu', () => {
|
|||||||
// Verify both link options are shown
|
// Verify both link options are shown
|
||||||
expect(screen.getByText('Link property to AD/LDAP')).toBeInTheDocument();
|
expect(screen.getByText('Link property to AD/LDAP')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Link property to SAML')).toBeInTheDocument();
|
expect(screen.getByText('Link property to SAML')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// TODO mock history and verify the link actions
|
it('hides LDAP and SAML link menu options for pending fields', async () => {
|
||||||
|
const pendingField = {
|
||||||
|
...baseField,
|
||||||
|
create_at: 0, // Mark as pending creation
|
||||||
|
};
|
||||||
|
|
||||||
|
renderComponent(pendingField);
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${pendingField.id}`);
|
||||||
|
fireEvent.click(menuButton);
|
||||||
|
|
||||||
|
// Verify both link options are not shown
|
||||||
|
expect(screen.queryByText('Link property to AD/LDAP')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Link property to SAML')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Edit link with" text when LDAP attribute is linked', async () => {
|
||||||
|
const linkedField = {
|
||||||
|
...baseField,
|
||||||
|
attrs: {
|
||||||
|
...baseField.attrs,
|
||||||
|
ldap: 'employeeID',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderComponent(linkedField);
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${linkedField.id}`);
|
||||||
|
fireEvent.click(menuButton);
|
||||||
|
|
||||||
|
// Verify the LDAP link text shows the linked property
|
||||||
|
expect(screen.getByText('Edit link with:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('AD/LDAP: employeeID')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Edit link with" text when SAML attribute is linked', async () => {
|
||||||
|
const linkedField = {
|
||||||
|
...baseField,
|
||||||
|
attrs: {
|
||||||
|
...baseField.attrs,
|
||||||
|
saml: 'position',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderComponent(linkedField);
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
const menuButton = screen.getByTestId(`user-property-field_dotmenu-${linkedField.id}`);
|
||||||
|
fireEvent.click(menuButton);
|
||||||
|
|
||||||
|
// Verify the SAML link text shows the linked property
|
||||||
|
expect(screen.getByText('Edit link with:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('SAML: position')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles field duplication', async () => {
|
it('handles field duplication', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import * as Menu from 'components/menu';
|
|||||||
import './user_properties_dot_menu.scss';
|
import './user_properties_dot_menu.scss';
|
||||||
import {useUserPropertyFieldDelete} from './user_properties_delete_modal';
|
import {useUserPropertyFieldDelete} from './user_properties_delete_modal';
|
||||||
import {isCreatePending} from './user_properties_utils';
|
import {isCreatePending} from './user_properties_utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
field: UserPropertyField;
|
field: UserPropertyField;
|
||||||
canCreate: boolean;
|
canCreate: boolean;
|
||||||
@@ -174,28 +176,50 @@ const DotMenu = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.LinkItem
|
{field.create_at !== 0 && ([
|
||||||
id={`${menuId}_link_ad-ldap`}
|
<Menu.LinkItem
|
||||||
to={`/admin_console/authentication/ldap#custom_profile_attribute-${field.name}`}
|
key={`${menuId}_link_ad-ldap`}
|
||||||
leadingElement={<SyncIcon size={18}/>}
|
id={`${menuId}_link_ad-ldap`}
|
||||||
labels={(
|
to={`/admin_console/authentication/ldap#custom_profile_attribute-${field.name}`}
|
||||||
<FormattedMessage
|
leadingElement={<SyncIcon size={18}/>}
|
||||||
id='admin.system_properties.user_properties.dotmenu.ad_ldap.link_property.label'
|
labels={field.attrs.ldap ? (
|
||||||
defaultMessage={'Link property to AD/LDAP'}
|
<FormattedMessage
|
||||||
/>
|
id='admin.system_properties.user_properties.dotmenu.ad_ldap.edit_link.label'
|
||||||
)}
|
defaultMessage={'Edit link with: <Chip>AD/LDAP: {propertyName}</Chip>'}
|
||||||
/>
|
values={{
|
||||||
<Menu.LinkItem
|
Chip: (chunks: React.ReactNode) => <Chip>{chunks}</Chip>,
|
||||||
id={`${menuId}_link_ad-ldap`}
|
propertyName: field.attrs.ldap,
|
||||||
to={`/admin_console/authentication/saml#custom_profile_attribute-${field.name}`}
|
}}
|
||||||
leadingElement={<SyncIcon size={18}/>}
|
/>
|
||||||
labels={(
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.system_properties.user_properties.dotmenu.saml.link_property.label'
|
id='admin.system_properties.user_properties.dotmenu.ad_ldap.link_property.label'
|
||||||
defaultMessage={'Link property to SAML'}
|
defaultMessage={'Link property to AD/LDAP'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>,
|
||||||
|
<Menu.LinkItem
|
||||||
|
key={`${menuId}_link_saml`}
|
||||||
|
id={`${menuId}_link_saml`}
|
||||||
|
to={`/admin_console/authentication/saml#custom_profile_attribute-${field.name}`}
|
||||||
|
leadingElement={<SyncIcon size={18}/>}
|
||||||
|
labels={field.attrs.saml ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.system_properties.user_properties.dotmenu.saml.edit_link.label'
|
||||||
|
defaultMessage={'Edit link with: <Chip>SAML: {propertyName}</Chip>'}
|
||||||
|
values={{
|
||||||
|
Chip: (chunks: React.ReactNode) => <Chip>{chunks}</Chip>,
|
||||||
|
propertyName: field.attrs.saml,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.system_properties.user_properties.dotmenu.saml.link_property.label'
|
||||||
|
defaultMessage={'Link property to SAML'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
])}
|
||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
{canCreate && (
|
{canCreate && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -226,4 +250,13 @@ const DotMenu = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Chip = ({children, ...rest}: ComponentProps<'span'>) => (
|
||||||
|
<span
|
||||||
|
className='user-property-field-dotmenu__chip'
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
export default DotMenu;
|
export default DotMenu;
|
||||||
|
|||||||
@@ -2729,10 +2729,12 @@
|
|||||||
"admin.system_properties.details.saving_changes": "Saving configuration…",
|
"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.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.add_property": "Add property",
|
||||||
|
"admin.system_properties.user_properties.dotmenu.ad_ldap.edit_link.label": "Edit link with: <Chip>AD/LDAP: {propertyName}</Chip>",
|
||||||
"admin.system_properties.user_properties.dotmenu.ad_ldap.link_property.label": "Link property to AD/LDAP",
|
"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.delete.label": "Delete property",
|
||||||
"admin.system_properties.user_properties.dotmenu.duplicate.label": "Duplicate 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.duplicate.name_copy": "{fieldName} (copy)",
|
||||||
|
"admin.system_properties.user_properties.dotmenu.saml.edit_link.label": "Edit link with: <Chip>SAML: {propertyName}</Chip>",
|
||||||
"admin.system_properties.user_properties.dotmenu.saml.link_property.label": "Link property to SAML",
|
"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.always.label": "Always show",
|
||||||
"admin.system_properties.user_properties.dotmenu.visibility.hidden.label": "Always hide",
|
"admin.system_properties.user_properties.dotmenu.visibility.hidden.label": "Always hide",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
color: functions.v(center-channel-color);
|
color: functions.v(center-channel-color);
|
||||||
grid-area: center;
|
grid-area: center;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user