Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const appRoot = mockAppRoot()
Save: 'Save',
Required_field: '{{field}} is required',
})
.withEndpoint('GET', '/v1/abac/attributes/:key/is-in-use', async () => ({ inUse: false }))
.build();

const FormProviderWrapper = ({ children, defaultValues }: { children: ReactNode; defaultValues?: Partial<AttributesFormFormData> }) => {
Expand Down Expand Up @@ -255,4 +256,33 @@ describe('AttributesForm', () => {
const trashButtons = screen.getAllByRole('button', { name: 'ABAC_Remove_attribute' });
expect(trashButtons).toHaveLength(1);
});

it('should show disclaimer when trying to delete a locked attribute value that is in use', async () => {
const defaultValues = {
name: 'Test Attribute',
lockedAttributes: [{ value: 'Value 1' }, { value: 'Value 2' }],
};
render(
<FormProviderWrapper defaultValues={defaultValues}>
<AttributesForm {...defaultProps} />
</FormProviderWrapper>,
{
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/abac/attributes/:key/is-in-use', async () => ({ inUse: true }))
.withTranslations('en', 'core', {
ABAC_Cannot_delete_attribute_value_in_use: 'Cannot delete attribute value assigned to rooms. <1>View rooms</1>',
})
.build(),
},
);

const trashButtons = screen.getAllByRole('button', { name: 'ABAC_Remove_attribute' });
await userEvent.click(trashButtons[0]);

await waitFor(() => {
expect(screen.getByText('Cannot delete attribute value assigned to rooms.')).toBeInTheDocument();
});

expect(screen.getByText('Cannot delete attribute value assigned to rooms.')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import {
IconButton,
TextInput,
} from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { ContextualbarScrollableContent } from '@rocket.chat/ui-client';
import { useCallback, useId, useMemo, Fragment } from 'react';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback, useId, useMemo, Fragment, useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

import { useViewRoomsAction } from '../hooks/useViewRoomsAction';

export type AttributesFormFormData = {
name: string;
Expand All @@ -33,14 +37,17 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps)
register,
formState: { errors, isDirty },
watch,
getValues,
} = useFormContext<AttributesFormFormData>();

const { t } = useTranslation();

const attributeValues = watch('attributeValues');
const lockedAttributes = watch('lockedAttributes');

const { fields: lockedAttributesFields, remove: removeLockedAttribute } = useFieldArray({
const isAttributeUsed = useEndpoint('GET', '/v1/abac/attributes/:key/is-in-use', { key: getValues('name') });

const { fields: lockedAttributesFields, remove: removeLockedAttributeField } = useFieldArray({
name: 'lockedAttributes',
});

Expand All @@ -64,6 +71,20 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps)
const nameField = useId();
const valuesField = useId();

const [showDisclaimer, setShowDisclaimer] = useState<number[]>([]);
const viewRoomsAction = useViewRoomsAction();

const removeLockedAttribute = useEffectEvent(async (index: number) => {
const isInUse = await isAttributeUsed();
if (showDisclaimer.includes(index)) {
return;
}
if (isInUse?.inUse) {
return setShowDisclaimer((prev) => [...prev, index]);
}
return removeLockedAttributeField(index);
});

const getAttributeValuesError = useCallback(() => {
if (errors.attributeValues?.length && errors.attributeValues?.length > 0) {
return errors.attributeValues[0]?.value?.message;
Expand Down Expand Up @@ -118,6 +139,26 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps)
)}
</FieldRow>
{errors.lockedAttributes?.[index]?.value && <FieldError>{errors.lockedAttributes?.[index]?.value?.message}</FieldError>}
{showDisclaimer.includes(index) && (
<FieldError>
<Trans
i18nKey='ABAC_Cannot_delete_attribute_value_in_use'
components={{
1: (
<Box
is='a'
onClick={(e) => {
e.preventDefault();
viewRoomsAction(getValues('name'));
}}
>
{t('ABAC_View_rooms')}
</Box>
),
}}
/>
</FieldError>
)}
</Fragment>
))}
{fields.map((field, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
GenericTableRow,
usePagination,
} from '@rocket.chat/ui-client';
import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
import { useEndpoint, useRouter, useSearchParameter } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -22,7 +22,9 @@ import { useIsABACAvailable } from '../hooks/useIsABACAvailable';
const AttributesPage = () => {
const { t } = useTranslation();

const [text, setText] = useState('');
const searchTerm = useSearchParameter('searchTerm');
const [text, setText] = useState(searchTerm ?? '');

const debouncedText = useDebouncedValue(text, 200);
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
const getAttributes = useEndpoint('GET', '/v1/abac/attributes');
Expand Down
11 changes: 7 additions & 4 deletions apps/meteor/client/views/admin/ABAC/ABACRoomsTab/RoomsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
GenericTableRow,
usePagination,
} from '@rocket.chat/ui-client';
import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
import { useEndpoint, useRouter, useSearchParameter } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -21,15 +21,18 @@ import { useIsABACAvailable } from '../hooks/useIsABACAvailable';

const RoomsPage = () => {
const { t } = useTranslation();
const router = useRouter();

const searchTerm = useSearchParameter('searchTerm');
const searchType = useSearchParameter('type') as 'roomName' | 'attribute' | 'value';

const [text, setText] = useState('');
const [filterType, setFilterType] = useState<'all' | 'roomName' | 'attribute' | 'value'>('all');
const [text, setText] = useState(searchTerm ?? '');
const [filterType, setFilterType] = useState<'all' | 'roomName' | 'attribute' | 'value'>(searchType ?? 'all');
const debouncedText = useDebouncedValue(text, 200);
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
const getRooms = useEndpoint('GET', '/v1/abac/rooms');
const isABACAvailable = useIsABACAvailable();

const router = useRouter();
const handleNewAttribute = useEffectEvent(() => {
router.navigate({
name: 'admin-ABAC',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Trans, useTranslation } from 'react-i18next';

import { useIsABACAvailable } from './useIsABACAvailable';
import { useViewRoomsAction } from './useViewRoomsAction';
import { ABACQueryKeys } from '../../../../lib/queryKeys';

export const useAttributeOptions = (attribute: { _id: string; key: string }): GenericMenuItemProps[] => {
Expand All @@ -18,6 +19,7 @@ export const useAttributeOptions = (attribute: { _id: string; key: string }): Ge
const isAttributeUsed = useEndpoint('GET', '/v1/abac/attributes/:key/is-in-use', { key: attribute.key });
const dispatchToastMessage = useToastMessageDispatch();
const isABACAvailable = useIsABACAvailable();
const viewRoomsAction = useViewRoomsAction();

const editAction = useEffectEvent(() => {
return router.navigate(
Expand Down Expand Up @@ -56,8 +58,10 @@ export const useAttributeOptions = (attribute: { _id: string; key: string }): Ge
icon={null}
title={t('ABAC_Cannot_delete_attribute')}
confirmText={t('View_rooms')}
// TODO Route to rooms tab once implemented
onConfirm={() => setModal(null)}
onConfirm={() => {
viewRoomsAction(attribute.key);
setModal(null);
}}
onCancel={() => setModal(null)}
>
<Trans
Expand Down
23 changes: 23 additions & 0 deletions apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useRouter } from '@rocket.chat/ui-contexts';

export const useViewRoomsAction = () => {
const router = useRouter();
return useEffectEvent((key: string) => {
return router.navigate(
{
name: 'admin-ABAC',
params: {
tab: 'rooms',
context: '',
id: '',
},
search: {
searchTerm: key,
type: 'attribute',
},
},
{ replace: true },
);
});
};
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"ABAC_Element_Name": "Element Name",
"ABAC_Cannot_delete_attribute": "Cannot delete attribute assigned to rooms",
"ABAC_Cannot_delete_attribute_content": "Unassign <bold>{{attributeName}}</bold> from all rooms before attempting to delete it.",
"ABAC_Cannot_delete_attribute_value_in_use": "Cannot delete attribute value assigned to rooms. <1>View rooms</1>",
"ABAC_Delete_room_attribute": "Delete attribute",
"ABAC_Delete_room_attribute_content": "Are you sure you want to delete <bold>{{attributeName}}</bold> ?<br/>Existing rooms will not be affected as it is not currently assigned to any.",
"ABAC_Attribute_created": "{{attributeName}} attribute created",
Expand Down
Loading