This reverts commit d838f74273.
This commit is contained in:
parent
d838f74273
commit
934ed2a773
21 changed files with 5 additions and 1012 deletions
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
/**
|
||||
* Fetches the channel access control attributes for a specific channel
|
||||
* @param serverUrl - The server URL
|
||||
* @param channelId - The ID of the channel
|
||||
* @returns The channel access control attributes or an error
|
||||
*/
|
||||
export async function fetchChannelAccessControlAttributes(serverUrl: string, channelId: string) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const attributes = await client.getChannelAccessControlAttributes(channelId);
|
||||
return {attributes};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchChannelAccessControlAttributes', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,6 @@ export interface ClientChannelsMix {
|
|||
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
|
||||
getChannelStats: (channelId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelStats>;
|
||||
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
|
||||
getChannelAccessControlAttributes: (channelId: string) => Promise<ChannelAccessControlAttributes>;
|
||||
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => Promise<any>;
|
||||
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
|
|
@ -288,13 +287,6 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getChannelAccessControlAttributes = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/access_control/attributes`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => {
|
||||
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
|
||||
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, render} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
// We're mocking useTheme but not using it directly
|
||||
|
||||
import AlertBanner from './alert_banner';
|
||||
|
||||
jest.mock('@context/theme', () => ({
|
||||
useTheme: jest.fn(() => ({
|
||||
sidebarTextActiveBorder: '#1C58D9',
|
||||
awayIndicator: '#F5AB1E',
|
||||
dndIndicator: '#D24B4E',
|
||||
centerChannelColor: '#3F4350',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('components/alert_banner', () => {
|
||||
const baseProps = {
|
||||
type: 'info' as const,
|
||||
message: 'Test message',
|
||||
testID: 'test-banner',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render correctly with base props', () => {
|
||||
const {getByTestId, getByText} = render(
|
||||
<AlertBanner
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('test-banner')).toBeTruthy();
|
||||
expect(getByTestId('test-banner-icon')).toBeTruthy();
|
||||
expect(getByText('Test message')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render description when provided', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
description: 'Test description',
|
||||
};
|
||||
|
||||
const {getByText} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
expect(getByText('Test description')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render tags when provided', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
tags: ['Tag1', 'Tag2'],
|
||||
};
|
||||
|
||||
const {getByText} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
expect(getByText('Tag1')).toBeTruthy();
|
||||
expect(getByText('Tag2')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render dismiss button when isDismissable is true', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isDismissable: true,
|
||||
onDismiss: jest.fn(),
|
||||
};
|
||||
|
||||
const {getByTestId} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('test-banner-dismiss')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call onDismiss when dismiss button is pressed', () => {
|
||||
const onDismiss = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
isDismissable: true,
|
||||
onDismiss,
|
||||
};
|
||||
|
||||
const {getByTestId} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('test-banner-dismiss'));
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onPress when banner is pressed', () => {
|
||||
const onPress = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
onPress,
|
||||
};
|
||||
|
||||
const {getByTestId} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('test-banner').children[0]);
|
||||
expect(onPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render with warning type', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
type: 'warning' as const,
|
||||
};
|
||||
|
||||
const {getByTestId} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('test-banner')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with error type', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
type: 'error' as const,
|
||||
};
|
||||
|
||||
const {getByTestId} = render(
|
||||
<AlertBanner {...props}/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('test-banner')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import Tag from './tag';
|
||||
|
||||
type AlertBannerProps = {
|
||||
type: 'info' | 'warning' | 'error';
|
||||
message: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
isDismissable?: boolean;
|
||||
onDismiss?: () => void;
|
||||
onPress?: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const iconByType = {
|
||||
info: 'information-outline',
|
||||
warning: 'alert-outline',
|
||||
error: 'alert-circle-outline',
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderTopWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
body: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
marginLeft: 8,
|
||||
},
|
||||
message: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
description: {
|
||||
...typography('Body', 100, 'Regular'),
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 4,
|
||||
},
|
||||
tagsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 8,
|
||||
},
|
||||
dismissIcon: {
|
||||
marginLeft: 8,
|
||||
padding: 4,
|
||||
},
|
||||
|
||||
// Info styles
|
||||
infoContainer: {
|
||||
borderColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.08),
|
||||
},
|
||||
infoIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
|
||||
// Warning styles
|
||||
warningContainer: {
|
||||
borderColor: changeOpacity(theme.awayIndicator, 0.16),
|
||||
backgroundColor: changeOpacity(theme.awayIndicator, 0.08),
|
||||
},
|
||||
warningIcon: {
|
||||
color: theme.awayIndicator,
|
||||
},
|
||||
|
||||
// Error styles
|
||||
errorContainer: {
|
||||
borderColor: changeOpacity(theme.dndIndicator, 0.16),
|
||||
backgroundColor: changeOpacity(theme.dndIndicator, 0.08),
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AlertBanner = ({
|
||||
type = 'info',
|
||||
message,
|
||||
description,
|
||||
tags,
|
||||
isDismissable = false,
|
||||
onDismiss,
|
||||
onPress,
|
||||
testID,
|
||||
}: AlertBannerProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
const iconName = iconByType[type];
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
if (onDismiss) {
|
||||
onDismiss();
|
||||
}
|
||||
}, [onDismiss]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.container,
|
||||
styles[`${type}Container`],
|
||||
], [styles, type]);
|
||||
|
||||
const iconStyle = useMemo(() => [
|
||||
styles[`${type}Icon`],
|
||||
], [styles, type]);
|
||||
|
||||
const ContentWrapper = onPress ? TouchableOpacity : View;
|
||||
const contentWrapperProps = onPress ? {onPress} : {};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
testID={testID}
|
||||
accessibilityRole='alert'
|
||||
>
|
||||
<ContentWrapper
|
||||
style={styles.content}
|
||||
{...contentWrapperProps}
|
||||
>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={20}
|
||||
style={iconStyle}
|
||||
testID={testID ? (testID + '-icon') : undefined}
|
||||
accessibilityLabel={`${type} icon`}
|
||||
/>
|
||||
<View style={styles.body}>
|
||||
<Text style={styles.message}>
|
||||
{message}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={styles.description}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
{tags && tags.length > 0 && (
|
||||
<View style={styles.tagsContainer}>
|
||||
{tags.map((tag, index) => (
|
||||
<Tag
|
||||
key={tag + '-' + index}
|
||||
text={tag}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{isDismissable && onDismiss && (
|
||||
<Pressable
|
||||
onPress={handleDismiss}
|
||||
style={styles.dismissIcon}
|
||||
testID={testID ? (testID + '-dismiss') : undefined}
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel='Dismiss'
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={16}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertBanner;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AlertBanner from './alert_banner';
|
||||
import Tag from './tag';
|
||||
|
||||
export default AlertBanner;
|
||||
export {Tag};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {render} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import Tag from './tag';
|
||||
|
||||
jest.mock('@context/theme', () => ({
|
||||
useTheme: jest.fn(() => ({
|
||||
centerChannelColor: '#3F4350',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('components/alert_banner/tag', () => {
|
||||
it('should render correctly with text', () => {
|
||||
const {getByText} = render(
|
||||
<Tag
|
||||
text='Test Tag'
|
||||
type='info'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByText('Test Tag')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have accessibility role', () => {
|
||||
const {getByRole} = render(
|
||||
<Tag
|
||||
text='Test Tag'
|
||||
type='info'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByRole('text')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type TagProps = {
|
||||
text: string;
|
||||
type: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
tag: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
tagText: {
|
||||
...typography('Heading', 75, 'SemiBold'),
|
||||
},
|
||||
|
||||
// All tag types use the same styling based on design requirements
|
||||
tagBackground: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
tagTextColor: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Tag = ({text}: TagProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.tag, styles.tagBackground]}
|
||||
accessibilityRole='text'
|
||||
>
|
||||
<Text style={[styles.tagText, styles.tagTextColor]}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
|
@ -11,17 +11,6 @@ import {MM_TABLES} from '@constants/database';
|
|||
const {CHANNEL_BOOKMARK, CHANNEL_INFO, DRAFT, FILE, POST, CHANNEL, CUSTOM_PROFILE_ATTRIBUTE, CUSTOM_PROFILE_FIELD, SCHEDULED_POST} = MM_TABLES.SERVER;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 11,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: CHANNEL,
|
||||
columns: [
|
||||
{name: 'abac_policy_enforced', type: 'boolean', isOptional: true},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 10,
|
||||
steps: [
|
||||
|
|
|
|||
|
|
@ -112,9 +112,6 @@ export default class ChannelModel extends Model implements ChannelModelInterface
|
|||
/** bannerInfo : The banner information for the channel */
|
||||
@json('banner_info', safeParseJSON) bannerInfo?: ChannelBannerInfo;
|
||||
|
||||
/** policy_enforced : Whether the Attribute-Based Access Control (ABAC) policy is enforced for this channel, controlling access based on user attributes */
|
||||
@field('abac_policy_enforced') abacPolicyEnforced?: boolean;
|
||||
|
||||
/** members : Users belonging to this channel */
|
||||
@children(CHANNEL_MEMBERSHIP) members!: Query<ChannelMembershipModel>;
|
||||
|
||||
|
|
@ -166,7 +163,6 @@ export default class ChannelModel extends Model implements ChannelModelInterface
|
|||
group_constrained: null,
|
||||
shared: this.shared,
|
||||
banner_info: this.bannerInfo,
|
||||
policy_enforced: this.abacPolicyEnforced,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ export const transformChannelRecord = ({action, database, value}: TransformerArg
|
|||
channel.teamId = raw.team_id;
|
||||
channel.type = raw.type;
|
||||
channel.bannerInfo = raw.banner_info;
|
||||
channel.abacPolicyEnforced = Boolean(raw.policy_enforced);
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import {
|
|||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 11,
|
||||
version: 10,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,5 @@ export default tableSchema({
|
|||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'banner_info', type: 'string', isOptional: true},
|
||||
{name: 'abac_policy_enforced', type: 'boolean', isOptional: true},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const {
|
|||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 11,
|
||||
version: 10,
|
||||
unsafeSql: undefined,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
|
|
@ -126,7 +126,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
type: {name: 'type', type: 'string'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
banner_info: {name: 'banner_info', type: 'string', isOptional: true},
|
||||
abac_policy_enforced: {name: 'abac_policy_enforced', type: 'boolean', isOptional: true},
|
||||
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'create_at', type: 'number'},
|
||||
|
|
@ -140,7 +140,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'banner_info', type: 'string', isOptional: true},
|
||||
{name: 'abac_policy_enforced', type: 'boolean', isOptional: true},
|
||||
],
|
||||
},
|
||||
[CHANNEL_BOOKMARK]: {
|
||||
|
|
|
|||
|
|
@ -1,328 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import * as ChannelAccessControlAttributesActions from '@actions/remote/channel_access_control_attributes';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
// Access the module-level cache to reset it between tests
|
||||
// This is necessary because the cache persists between test runs
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
import * as AccessControlAttributesModule from './access_control_attributes';
|
||||
import {useAccessControlAttributes} from './access_control_attributes';
|
||||
|
||||
const MOCK_SERVER_URL = 'https://test-server.mattermost.com';
|
||||
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn(() => MOCK_SERVER_URL),
|
||||
}));
|
||||
|
||||
jest.mock('@actions/remote/channel_access_control_attributes', () => ({
|
||||
fetchChannelAccessControlAttributes: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useAccessControlAttributes', () => {
|
||||
const mockServerUrl = MOCK_SERVER_URL;
|
||||
const mockEntityId = 'channel-id';
|
||||
const mockAttributes = {
|
||||
groups: ['group1', 'group2'],
|
||||
locations: ['location1'],
|
||||
departments: ['department1', 'department2', 'department3'],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(useServerUrl as jest.Mock).mockReturnValue(mockServerUrl);
|
||||
|
||||
// Clear the module-level cache before each test
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
if (AccessControlAttributesModule.attributesCache) {
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
Object.keys(AccessControlAttributesModule.attributesCache).forEach((key) => {
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
delete AccessControlAttributesModule.attributesCache[key];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty tags when abac policy is not enforced', async () => {
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, false));
|
||||
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty tags when entityId is undefined', async () => {
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', undefined, true));
|
||||
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch and process attributes when abac policy is enforced', async () => {
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue({
|
||||
attributes: mockAttributes,
|
||||
});
|
||||
|
||||
const {result, waitForNextUpdate} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Initial state might be false if the hook sets loading after the first render
|
||||
// expect(result.current.loading).toBe(true);
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
|
||||
// Wait for the fetch to complete
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Check that the fetch was called with the correct parameters
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).toHaveBeenCalledWith(
|
||||
mockServerUrl,
|
||||
mockEntityId,
|
||||
);
|
||||
|
||||
// Check that the attributes were processed correctly
|
||||
expect(result.current.attributeTags).toEqual(['group1', 'group2', 'location1', 'department1', 'department2', 'department3']);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle errors when fetching attributes', async () => {
|
||||
const mockError = new Error('Failed to fetch attributes');
|
||||
|
||||
// Reset all mocks to ensure a clean state
|
||||
jest.resetAllMocks();
|
||||
|
||||
// Use mockRejectedValueOnce instead of mockImplementation for more explicit rejection
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockRejectedValueOnce(mockError);
|
||||
|
||||
// Render the hook
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Manually trigger a fetch with forceRefresh to bypass cache
|
||||
await act(async () => {
|
||||
await result.current.fetchAttributes(true);
|
||||
});
|
||||
|
||||
// Check that the error was handled correctly
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(mockError);
|
||||
});
|
||||
|
||||
it('should handle null or undefined values in attributes', async () => {
|
||||
// Clear any previous mock implementations
|
||||
jest.resetAllMocks();
|
||||
|
||||
// Set up the mock to return data with null/undefined values
|
||||
const mockDataWithNulls = {
|
||||
attributes: {
|
||||
groups: ['group1', null, undefined, 'group2'],
|
||||
locations: null,
|
||||
departments: undefined,
|
||||
},
|
||||
};
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue(mockDataWithNulls);
|
||||
|
||||
// Render the hook with act to handle all state updates
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Wait for the initial fetch to complete
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
// Check that null and undefined values were filtered out
|
||||
expect(result.current.attributeTags).toEqual(['group1', 'group2']);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle non-object attributes', async () => {
|
||||
// Clear any previous mock implementations
|
||||
jest.resetAllMocks();
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValueOnce({
|
||||
attributes: 'not an object',
|
||||
});
|
||||
|
||||
// Render the hook
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Manually trigger a fetch with forceRefresh to bypass cache
|
||||
await act(async () => {
|
||||
await result.current.fetchAttributes(true);
|
||||
});
|
||||
|
||||
// Check that non-object attributes result in empty tags
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle non-array values in attributes', async () => {
|
||||
// Clear any previous mock implementations
|
||||
jest.resetAllMocks();
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValueOnce({
|
||||
attributes: {
|
||||
groups: 'not an array',
|
||||
locations: 123,
|
||||
departments: {key: 'value'},
|
||||
},
|
||||
});
|
||||
|
||||
// Render the hook
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Manually trigger a fetch with forceRefresh to bypass cache
|
||||
await act(async () => {
|
||||
await result.current.fetchAttributes(true);
|
||||
});
|
||||
|
||||
// Check that non-array values were handled correctly
|
||||
expect(result.current.attributeTags).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
// This test verifies that the hook correctly handles dependency changes
|
||||
it('should refetch when dependencies change', async () => {
|
||||
// Mock the API call to return different data for different entity IDs
|
||||
const firstEntityData = {
|
||||
attributes: {
|
||||
groups: ['group1', 'group2'],
|
||||
},
|
||||
};
|
||||
|
||||
const secondEntityData = {
|
||||
attributes: {
|
||||
departments: ['department1', 'department2'],
|
||||
},
|
||||
};
|
||||
|
||||
// Set up the mock to return the first data set
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue(firstEntityData);
|
||||
|
||||
// Render the hook with the first entity ID and manually trigger a fetch
|
||||
const {result: result1} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Manually trigger a fetch to avoid waiting for the automatic fetch
|
||||
await act(async () => {
|
||||
await result1.current.fetchAttributes(true); // Use forceRefresh to bypass cache
|
||||
});
|
||||
|
||||
// Verify the API was called with the correct parameters
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).toHaveBeenCalledWith(
|
||||
mockServerUrl,
|
||||
mockEntityId,
|
||||
);
|
||||
|
||||
// Verify the correct data was processed
|
||||
expect(result1.current.attributeTags).toEqual(['group1', 'group2']);
|
||||
|
||||
// Reset mocks for the second test
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up the mock to return the second data set
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue(secondEntityData);
|
||||
|
||||
// Render the hook with a different entity ID
|
||||
const newEntityId = 'new-channel-id';
|
||||
const {result: result2} = renderHook(() => useAccessControlAttributes('channel', newEntityId, true));
|
||||
|
||||
// Manually trigger a fetch
|
||||
await act(async () => {
|
||||
await result2.current.fetchAttributes(true); // Use forceRefresh to bypass cache
|
||||
});
|
||||
|
||||
// Verify the API was called with the new entity ID
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).toHaveBeenCalledWith(
|
||||
mockServerUrl,
|
||||
newEntityId,
|
||||
);
|
||||
|
||||
// Verify the correct data was processed
|
||||
expect(result2.current.attributeTags).toEqual(['department1', 'department2']);
|
||||
});
|
||||
|
||||
it('should use cached data when available and within TTL', async () => {
|
||||
// Mock Date.now to control time
|
||||
const originalDateNow = Date.now;
|
||||
const mockNow = jest.fn();
|
||||
const currentTime = 1000;
|
||||
mockNow.mockReturnValue(currentTime);
|
||||
global.Date.now = mockNow;
|
||||
|
||||
try {
|
||||
// Directly manipulate the cache instead of relying on the hook to populate it
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
AccessControlAttributesModule.attributesCache = {};
|
||||
|
||||
// Create the cache key the same way the hook does
|
||||
const cacheKey = `${mockServerUrl}:channel:${mockEntityId}`;
|
||||
|
||||
// @ts-ignore - Accessing private module variable for testing
|
||||
AccessControlAttributesModule.attributesCache[cacheKey] = {
|
||||
processedTags: ['group1', 'group2', 'location1', 'department1', 'department2', 'department3'],
|
||||
timestamp: currentTime - 1000, // Set timestamp to 1 second ago
|
||||
};
|
||||
|
||||
// Mock the API call to return the same data as the cache
|
||||
// This is important because the hook will still try to fetch data when it mounts
|
||||
jest.resetAllMocks();
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue({
|
||||
attributes: mockAttributes,
|
||||
});
|
||||
|
||||
// Render the hook - it should use the cache we just set
|
||||
const {result} = renderHook(() => useAccessControlAttributes('channel', mockEntityId, true));
|
||||
|
||||
// Wait a bit to ensure the hook has time to process
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
// Check that the cached data was used
|
||||
expect(result.current.attributeTags).toEqual(['group1', 'group2', 'location1', 'department1', 'department2', 'department3']);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
|
||||
// The hook will still make an API call when it mounts, even if the cache is populated
|
||||
// So we just verify that the API call is made only once and that the hook uses the cached data
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance time beyond TTL
|
||||
mockNow.mockReturnValue(currentTime + (5 * 60 * 1000) + 1); // 5 minutes + 1ms later
|
||||
|
||||
// Reset mock call count and set up a different mock response for the second render
|
||||
jest.clearAllMocks();
|
||||
(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes as jest.Mock).mockResolvedValue({
|
||||
attributes: {
|
||||
differentData: ['should', 'not', 'be', 'used'],
|
||||
},
|
||||
});
|
||||
|
||||
// Render again - should fetch from API because cache expired
|
||||
const {result: result2} = renderHook(() =>
|
||||
useAccessControlAttributes('channel', mockEntityId, true),
|
||||
);
|
||||
|
||||
// Wait for the hook to update
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
expect(ChannelAccessControlAttributesActions.fetchChannelAccessControlAttributes).toHaveBeenCalledTimes(1);
|
||||
expect(result2.current.attributeTags).toEqual(['should', 'not', 'be', 'used']);
|
||||
} finally {
|
||||
// Restore original Date.now
|
||||
global.Date.now = originalDateNow;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {fetchChannelAccessControlAttributes} from '@actions/remote/channel_access_control_attributes';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
// Module-level cache for access control attributes
|
||||
// The cache stores processed tags with a timestamp to implement a TTL (time-to-live)
|
||||
const attributesCache: Record<string, {
|
||||
processedTags: string[];
|
||||
timestamp: number;
|
||||
}> = {};
|
||||
|
||||
// Cache TTL in milliseconds (5 minutes)
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* A hook for fetching access control attributes for a channel.
|
||||
*
|
||||
* @param entityType - The type of entity (e.g., 'channel')
|
||||
* @param entityId - The ID of the entity
|
||||
* @param hasAbacPolicyEnforced - Whether the entity has abac policy enforcement enabled
|
||||
* @returns An object containing the attribute tags, loading state, and fetch function
|
||||
*/
|
||||
export const useAccessControlAttributes = (
|
||||
entityType: 'channel',
|
||||
entityId: string | undefined,
|
||||
hasAbacPolicyEnforced: boolean | undefined,
|
||||
) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const [attributeTags, setAttributeTags] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Use useRef to track if the component is mounted
|
||||
const isMounted = useRef(true);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Helper function to process attribute data and extract tags
|
||||
const processAttributeData = useCallback((data: Record<string, string[]> | undefined) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tags: string[] = [];
|
||||
|
||||
// Process each entry in the attributes object
|
||||
for (const values of Object.values(data)) {
|
||||
if (Array.isArray(values)) {
|
||||
for (const value of values) {
|
||||
if (value !== undefined && value !== null) {
|
||||
tags.push(`${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}, []);
|
||||
|
||||
const fetchAttributes = useCallback(async (forceRefresh = false) => {
|
||||
if (!entityId || !hasAbacPolicyEnforced) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cacheKey = `${serverUrl}:${entityType}:${entityId}`;
|
||||
const cachedEntry = attributesCache[cacheKey];
|
||||
const now = Date.now();
|
||||
|
||||
// Use cache if it exists and is not too old and forceRefresh is false
|
||||
if (!forceRefresh && cachedEntry && (now - cachedEntry.timestamp < CACHE_TTL)) {
|
||||
// Use the pre-processed tags directly from the cache
|
||||
setAttributeTags(cachedEntry.processedTags);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no cache or cache expired, fetch from API
|
||||
if (entityType === 'channel') {
|
||||
const result = await fetchChannelAccessControlAttributes(serverUrl, entityId);
|
||||
|
||||
if (isMounted.current) {
|
||||
if (result?.attributes && typeof result.attributes === 'object') {
|
||||
// Process the data once
|
||||
const tags = processAttributeData(result.attributes);
|
||||
|
||||
// Store only the processed tags in cache
|
||||
attributesCache[cacheKey] = {
|
||||
processedTags: tags,
|
||||
timestamp: now,
|
||||
};
|
||||
|
||||
// Update the state
|
||||
setAttributeTags(tags);
|
||||
} else {
|
||||
setAttributeTags([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMounted.current) {
|
||||
setError(err as Error);
|
||||
setAttributeTags([]);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [entityType, entityId, hasAbacPolicyEnforced, serverUrl, processAttributeData]);
|
||||
|
||||
// Fetch attributes when the component mounts or when dependencies change
|
||||
useEffect(() => {
|
||||
fetchAttributes();
|
||||
}, [fetchAttributes]);
|
||||
|
||||
return {
|
||||
attributeTags,
|
||||
loading,
|
||||
error,
|
||||
fetchAttributes,
|
||||
};
|
||||
};
|
||||
|
|
@ -8,7 +8,6 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
|||
|
||||
import {addMembersToChannel} from '@actions/remote/channel';
|
||||
import {fetchProfilesNotInChannel, searchProfiles} from '@actions/remote/user';
|
||||
import AlertBanner from '@components/alert_banner';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import Search from '@components/search';
|
||||
|
|
@ -17,7 +16,6 @@ import ServerUserList from '@components/server_user_list';
|
|||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAccessControlAttributes} from '@hooks/access_control_attributes';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
|
|
@ -135,10 +133,6 @@ export default function ChannelAddMembers({
|
|||
const [term, setTerm] = useState('');
|
||||
const [addingMembers, setAddingMembers] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
// Use the hook to fetch access control attributes
|
||||
const {attributeTags} = useAccessControlAttributes('channel', channel?.id, channel?.abacPolicyEnforced);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
|
|
@ -245,7 +239,7 @@ export default function ChannelAddMembers({
|
|||
|
||||
useEffect(() => {
|
||||
updateNavigationButtons();
|
||||
}, [updateNavigationButtons, channel, serverUrl]);
|
||||
}, [updateNavigationButtons]);
|
||||
|
||||
if (addingMembers) {
|
||||
return (
|
||||
|
|
@ -264,23 +258,6 @@ export default function ChannelAddMembers({
|
|||
edges={['top', 'left', 'right']}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{showBanner && channel?.abacPolicyEnforced && (
|
||||
<AlertBanner
|
||||
type='info'
|
||||
message={formatMessage({
|
||||
id: 'channel.abac_policy_enforced.title',
|
||||
defaultMessage: 'Channel access is restricted by user attributes',
|
||||
})}
|
||||
description={formatMessage({
|
||||
id: 'channel.abac_policy_enforced.description',
|
||||
defaultMessage: 'Only people who match the specified access rules can be selected and added to this channel.',
|
||||
})}
|
||||
tags={attributeTags.length > 0 ? attributeTags : undefined}
|
||||
isDismissable={true}
|
||||
onDismiss={() => setShowBanner(false)}
|
||||
testID={`${TEST_ID}.alert_banner`}
|
||||
/>
|
||||
)}
|
||||
<View style={style.searchBar}>
|
||||
<Search
|
||||
testID={`${TEST_ID}.search_bar`}
|
||||
|
|
@ -319,3 +296,4 @@ export default function ChannelAddMembers({
|
|||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
|
||||
canChangeMemberRoles,
|
||||
teammateDisplayNameSetting,
|
||||
channel: currentChannel,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
|||
import {fetchChannelMemberships} from '@actions/remote/channel';
|
||||
import {fetchUsersByIds, searchProfiles} from '@actions/remote/user';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import AlertBanner from '@components/alert_banner';
|
||||
import Search from '@components/search';
|
||||
import UserList from '@components/user_list';
|
||||
import {Events, General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAccessControlAttributes} from '@hooks/access_control_attributes';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
|
|
@ -25,7 +23,6 @@ import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
|
|||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
|
|
@ -36,7 +33,6 @@ type Props = {
|
|||
currentUserId: string;
|
||||
tutorialWatched: boolean;
|
||||
teammateDisplayNameSetting: string;
|
||||
channel?: ChannelModel;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -82,7 +78,6 @@ export default function ManageChannelMembers({
|
|||
currentUserId,
|
||||
tutorialWatched,
|
||||
teammateDisplayNameSetting,
|
||||
channel: channelProp,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
|
|
@ -93,9 +88,6 @@ export default function ManageChannelMembers({
|
|||
const hasMoreProfiles = useRef(true);
|
||||
const pageRef = useRef(0);
|
||||
|
||||
// Use the hook to fetch access control attributes
|
||||
const {attributeTags} = useAccessControlAttributes('channel', channelId, channelProp?.abacPolicyEnforced);
|
||||
|
||||
const [isManageMode, setIsManageMode] = useState(false);
|
||||
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
|
||||
const [channelMembers, setChannelMembers] = useState<ChannelMembership[]>(EMPTY_MEMBERS);
|
||||
|
|
@ -302,22 +294,6 @@ export default function ManageChannelMembers({
|
|||
testID='manage_members.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{/* or if the channel has abac_policy_enforced=true */}
|
||||
{channelProp?.abacPolicyEnforced === true && (
|
||||
<AlertBanner
|
||||
type='info'
|
||||
message={formatMessage({
|
||||
id: 'channel.abac_policy_enforced.title',
|
||||
defaultMessage: 'Channel access is restricted by user attributes',
|
||||
})}
|
||||
description={formatMessage({
|
||||
id: 'channel.abac_policy_enforced.description',
|
||||
defaultMessage: 'Only people who match the specified access rules can be selected and added to this channel.',
|
||||
})}
|
||||
tags={attributeTags.length > 0 ? attributeTags : undefined}
|
||||
testID='manage_members.alert_banner'
|
||||
/>
|
||||
)}
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
autoCapitalize='none'
|
||||
|
|
|
|||
|
|
@ -233,8 +233,6 @@
|
|||
"channel_notification_preferences.reset_default": "Reset to default",
|
||||
"channel_notification_preferences.thread_replies": "Thread replies",
|
||||
"channel_notification_preferences.unmute_content": "Unmute channel",
|
||||
"channel.abac_policy_enforced.description": "Only people who match the specified access rules can be selected and added to this channel.",
|
||||
"channel.abac_policy_enforced.title": "Channel access is restricted by user attributes",
|
||||
"channel.banner.bottom_sheet.title": "Channel Banner",
|
||||
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
|
||||
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
|
||||
|
|
|
|||
5
types/api/channels.d.ts
vendored
5
types/api/channels.d.ts
vendored
|
|
@ -45,9 +45,6 @@ type Channel = {
|
|||
group_constrained: boolean|null;
|
||||
shared: boolean;
|
||||
banner_info?: ChannelBannerInfo;
|
||||
|
||||
/** Whether the channel has Attribute-Based Access Control (ABAC) policy enforcement enabled, controlling access based on user attributes */
|
||||
policy_enforced?: boolean;
|
||||
};
|
||||
type ChannelPatch = {
|
||||
name?: string;
|
||||
|
|
@ -173,5 +170,3 @@ type ChannelBannerInfo = {
|
|||
text?: string;
|
||||
background_color?: string;
|
||||
}
|
||||
|
||||
type ChannelAccessControlAttributes = Record<string, string[]>;
|
||||
|
|
|
|||
|
|
@ -56,9 +56,6 @@ declare class ChannelModel extends Model {
|
|||
|
||||
bannerInfo?: ChannelBannerInfo;
|
||||
|
||||
/** Whether the channel has Attribute-Based Access Control (ABAC) policy enforcement enabled, controlling access based on user attributes */
|
||||
abacPolicyEnforced?: boolean;
|
||||
|
||||
/** members : Users belonging to this channel */
|
||||
members: Query<ChannelMembershipModel>;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue