From 200fd4d7b976e5ad4b94dcdda2ea9e5e331bcd51 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Wed, 17 Dec 2025 14:10:34 -0300 Subject: [PATCH 1/4] fix: Show disclaimer when attribute value removal is not possible --- .../ABAC/ABACAttributesTab/AttributesForm.tsx | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx index 7e2cb90fba850..b9f6118da41f6 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx @@ -10,10 +10,12 @@ 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'; export type AttributesFormFormData = { name: string; @@ -33,6 +35,7 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) register, formState: { errors, isDirty }, watch, + getValues, } = useFormContext(); const { t } = useTranslation(); @@ -40,7 +43,9 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) 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', }); @@ -64,6 +69,19 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) const nameField = useId(); const valuesField = useId(); + const [showDisclaimer, setShowDisclaimer] = useState([]); + + 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; @@ -118,6 +136,20 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) )} {errors.lockedAttributes?.[index]?.value && {errors.lockedAttributes?.[index]?.value?.message}} + {showDisclaimer.includes(index) && ( + + null}> + {t('ABAC_View_rooms')} + + ), + }} + /> + + )} ))} {fields.map((field, index) => ( From d184806f9101200a566ab544d93b9edc77f920af Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Wed, 17 Dec 2025 16:20:32 -0300 Subject: [PATCH 2/4] fix: route to rooms tab when viewing rooms with attribute value in use --- .../ABAC/ABACAttributesTab/AttributesForm.tsx | 15 ++++++++--- .../ABAC/ABACAttributesTab/AttributesPage.tsx | 6 +++-- .../admin/ABAC/ABACRoomsTab/RoomsPage.tsx | 11 +++++--- .../admin/ABAC/hooks/useAttributeOptions.tsx | 7 +++++- .../admin/ABAC/hooks/useViewRoomsAction.ts | 25 +++++++++++++++++++ packages/i18n/src/locales/en.i18n.json | 1 + 6 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts diff --git a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx index b9f6118da41f6..33a366c216987 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx @@ -17,6 +17,8 @@ import { useCallback, useId, useMemo, Fragment, useState } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; +import useViewRoomsAction from '../hooks/useViewRoomsAction'; + export type AttributesFormFormData = { name: string; attributeValues: { value: string }[]; @@ -70,6 +72,7 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) const valuesField = useId(); const [showDisclaimer, setShowDisclaimer] = useState([]); + const viewRoomsAction = useViewRoomsAction(); const removeLockedAttribute = useEffectEvent(async (index: number) => { const isInUse = await isAttributeUsed(); @@ -140,9 +143,15 @@ const AttributesForm = ({ onSave, onCancel, description }: AttributesFormProps) null}> + components={{ + 1: ( + { + e.preventDefault(); + viewRoomsAction(getValues('name')); + }} + > {t('ABAC_View_rooms')} ), diff --git a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesPage.tsx b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesPage.tsx index a52d6d4a743c4..d77f9b083cf0c 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesPage.tsx @@ -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'; @@ -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'); diff --git a/apps/meteor/client/views/admin/ABAC/ABACRoomsTab/RoomsPage.tsx b/apps/meteor/client/views/admin/ABAC/ABACRoomsTab/RoomsPage.tsx index 7c6222531d8a8..99ca28c4c27a8 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACRoomsTab/RoomsPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACRoomsTab/RoomsPage.tsx @@ -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'; @@ -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', diff --git a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx index f9623076ab056..837b6fc5673fc 100644 --- a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx +++ b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx @@ -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[] => { @@ -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( @@ -57,7 +59,10 @@ export const useAttributeOptions = (attribute: { _id: string; key: string }): Ge 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)} > { + 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 }, + ); + }); +}; + +export default useViewRoomsAction; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3b063d1ffb9df..3ff1adebcb942 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -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 {{attributeName}} 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", "ABAC_Delete_room_attribute": "Delete attribute", "ABAC_Delete_room_attribute_content": "Are you sure you want to delete {{attributeName}} ?
Existing rooms will not be affected as it is not currently assigned to any.", "ABAC_Attribute_created": "{{attributeName}} attribute created", From fc10e5d6f6cf930d09c367a89da910efec676397 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Thu, 18 Dec 2025 10:47:33 -0300 Subject: [PATCH 3/4] tests: fix & add tests --- .../ABACAttributesTab/AttributesForm.spec.tsx | 30 +++++++++++++++++++ .../admin/ABAC/hooks/useAttributeOptions.tsx | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.spec.tsx b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.spec.tsx index 5cad3a2b227b1..84927e6427a5a 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.spec.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.spec.tsx @@ -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 }) => { @@ -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( + + + , + { + 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', + }) + .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(); + }); }); diff --git a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx index 837b6fc5673fc..03e3359b006bb 100644 --- a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx +++ b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx @@ -58,7 +58,6 @@ 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={() => { viewRoomsAction(attribute.key); setModal(null); From af912e8f064617864a9b1a68cd1ace194cd8fcd2 Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 19 Dec 2025 01:23:36 -0300 Subject: [PATCH 4/4] Use named export --- .../views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx | 2 +- .../client/views/admin/ABAC/hooks/useAttributeOptions.tsx | 2 +- .../client/views/admin/ABAC/hooks/useViewRoomsAction.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx index 33a366c216987..9e899452914fd 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACAttributesTab/AttributesForm.tsx @@ -17,7 +17,7 @@ import { useCallback, useId, useMemo, Fragment, useState } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; -import useViewRoomsAction from '../hooks/useViewRoomsAction'; +import { useViewRoomsAction } from '../hooks/useViewRoomsAction'; export type AttributesFormFormData = { name: string; diff --git a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx index 03e3359b006bb..dc1dcf3101883 100644 --- a/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx +++ b/apps/meteor/client/views/admin/ABAC/hooks/useAttributeOptions.tsx @@ -7,7 +7,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Trans, useTranslation } from 'react-i18next'; import { useIsABACAvailable } from './useIsABACAvailable'; -import useViewRoomsAction from './useViewRoomsAction'; +import { useViewRoomsAction } from './useViewRoomsAction'; import { ABACQueryKeys } from '../../../../lib/queryKeys'; export const useAttributeOptions = (attribute: { _id: string; key: string }): GenericMenuItemProps[] => { diff --git a/apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts b/apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts index 072be653253fd..7edcbbbc97c89 100644 --- a/apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts +++ b/apps/meteor/client/views/admin/ABAC/hooks/useViewRoomsAction.ts @@ -1,7 +1,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useRouter } from '@rocket.chat/ui-contexts'; -const useViewRoomsAction = () => { +export const useViewRoomsAction = () => { const router = useRouter(); return useEffectEvent((key: string) => { return router.navigate( @@ -21,5 +21,3 @@ const useViewRoomsAction = () => { ); }); }; - -export default useViewRoomsAction;