diff --git a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.test.tsx b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.test.tsx index c2af813ced3..96b22babdc9 100644 --- a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.test.tsx +++ b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.test.tsx @@ -1309,6 +1309,22 @@ describe('AttributeDetails', () => { expect(screen.queryByTestId('attributeTypeLockWrap')).not.toBeInTheDocument(); }); + it('locks adding an external source while a pending Applies-to resource is on the form, so linking cannot silently change the type', async () => { + mockLoadedField(makeTemplate({type: 'select', attrs: {display_name: 'Department', options: [{id: 'opt-1', name: 'Engineering'}]}}), [makeLinked('user', 'user-field')]); + renderEdit(); + await waitForForm(); + + expect(screen.getByTestId('attributeExternalSourceTrigger')).toBeDisabled(); + expect(screen.getByTestId('attributeExternalSourceTriggerLockWrap')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('attributeAppliesToRow-user-toggle')); + await userEvent.click(screen.getByTestId('attributeAppliesToRow-user-remove')); + + await waitFor(() => expect(screen.queryByTestId('attributeAppliesToRow-user')).not.toBeInTheDocument()); + expect(screen.getByTestId('attributeExternalSourceTrigger')).not.toBeDisabled(); + expect(screen.queryByTestId('attributeExternalSourceTriggerLockWrap')).not.toBeInTheDocument(); + }); + it('locks Name editing while the attribute is currently applied to a resource', async () => { mockLoadedField(makeTemplate(), [makeLinked('user', 'user-field')]); renderEdit(); diff --git a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.tsx b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.tsx index 27eab5d1118..1d20ef6bbae 100644 --- a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.tsx +++ b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_details.tsx @@ -1129,6 +1129,7 @@ function AttributeDetails({disabled = false}: Props): JSX.Element { fieldType={fieldType} onLink={handleLink} disabled={saving || disabled} + disableAdding={typeLockedByAppliesTo} /> diff --git a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.test.tsx b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.test.tsx index fb7b0678695..ac730b451c9 100644 --- a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.test.tsx +++ b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.test.tsx @@ -43,6 +43,15 @@ describe('AttributeExternalSource', () => { expect(screen.getByRole('menuitem', {name: /^SAML/})).toBeInTheDocument(); }); + it('disables the add-source trigger when disableAdding is set, without disabling an existing chip\'s edit/remove actions', () => { + renderComponent({ldapAttr: 'department', disableAdding: true}); + + expect(screen.getByTestId('attributeExternalSourceTrigger')).toBeDisabled(); + expect(screen.getByTestId('attributeExternalSourceTriggerLockWrap')).toBeInTheDocument(); + expect(screen.getByTestId('attributeExternalSourceChip-ldap-edit')).not.toBeDisabled(); + expect(screen.getByTestId('attributeExternalSourceChip-ldap-remove')).not.toBeDisabled(); + }); + it('renders a chip for a linked source prefixed by Synced with, and offers only the remaining source', async () => { renderComponent({ldapAttr: 'department'}); diff --git a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.tsx b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.tsx index 3db10c1fabb..8dfdc15a7c2 100644 --- a/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.tsx +++ b/webapp/channels/src/components/admin_console/global_attributes/attribute_details/attribute_external_source.tsx @@ -9,6 +9,7 @@ import {components} from 'react-select'; import {PencilOutlineIcon, RefreshIcon, SyncIcon} from '@mattermost/compass-icons/components'; import {buttonClassNames} from '@mattermost/shared/components/button'; +import {WithTooltip} from '@mattermost/shared/components/tooltip'; import {openModal} from 'actions/views/modals'; @@ -34,13 +35,21 @@ type Props = { fieldType: AttributeFieldType; onLink: (source: ExternalSource, value: string) => void; disabled?: boolean; + + // Linking a new source forces fieldType to 'text' (see attribute_details.tsx's + // handleLink) -- while this attribute is applied to a resource, that would + // change its type out from under the server's type_change_with_dependents + // guard the same way the Type menu itself is locked for. Only gates the + // "add" trigger below: editing or removing an already-linked source never + // touches fieldType, so those stay enabled. + disableAdding?: boolean; }; function sourceValue(source: ExternalSource, ldapAttr: string, samlAttr: string): string { return source === 'ldap' ? ldapAttr : samlAttr; } -function AttributeExternalSource({ldapAttr, samlAttr, fieldType, onLink, disabled = false}: Props): JSX.Element { +function AttributeExternalSource({ldapAttr, samlAttr, fieldType, onLink, disabled = false, disableAdding = false}: Props): JSX.Element { const {formatMessage} = useIntl(); const dispatch = useDispatch(); @@ -156,41 +165,52 @@ function AttributeExternalSource({ldapAttr, samlAttr, fieldType, onLink, disable )} {unlinkedSources.length > 0 && ( -