mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
MM-65126: Allow admins to change CPA values for users from the system console (#33984)
* Update system_user_detail to enable editing of User Attributes
- This adds a `updateUserCustomProfileAttributesValues` method to client4 to allow updating attributes for a given userID, as a match to getUserCustomProfilesAttributesValues
- Fixed issue in saveCustomProfileAttributes where it used the version that did not take the passed in userId
* Fixes for tests
* Update saveCustomProfileAttribute mock in users.test.ts
The `saveCustomProfileAttribute` test was still using the old getCustomProfileAttributeValuesRoute instead of the same route that `getCustomProfileAttributeValues` uses.
* Adding e2e tests for editing User attributes (custom and system) in System Console
* Fix testing issue with selector for 'Authentication Method'
* workflows/server-ci-report.yml: security fixes by validating inputs (#33892)
* [MM-61899] Properly restrict users who previously shared a team from DMs/GMs when they no longer share a team. (#30094)
* [MM-61899] Properly restrict users who previously shared a team from DMs/GMs when they no longer share a team.
* Fix checks
* Fix test
* Fix i18n
* Added E2E tests
* Merge'd
* Add restricted DM check to more places
* Merge'd
* Restrict patching the channel (updating the channel)
* Update verbiage in the admin console
* Fix lint
* More tests
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
* Mirror Postgres 14 docker image (#34009)
* ugprade to go 1.24.6 (#34004)
* Bump Postgres minimum supported version to 14 (#34010)
* Downgrade French language (#33826)
* Downgrade French language
* Update i18n.jsx
* Updates buildFieldAttrs to preseve existing attrs when editing a field (#33991)
* Updates buildFieldAttrs to preseve existing attrs when editing a field
* Fix preserve option issue for select/multiselect type fields
* Fix linter
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* MM-64423 - Removing the clear func on switch (#31214)
* MM-64423 - Removing the clear func on switch
* Select all teams by default
* Updating test
* Updating test
* Removing showFilterHaveBeenReset
* Removing search string
* Do not clear the search term
* Updating tests
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
* Mm 65123 remove channel abac ff (#33953)
* MM-65123 - remove channel abac feature flag
* enable the channel scope access control to true
* fix linters
* adjust expected error in tests
* remove no longer needed comment
* Remove write_restrictable from core ABAC settings and fix channel access control logic
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
* [MM-65837], [MM-65824] - Update Dependencies (#33972)
* Update github.com/mholt/archives
* Update github.com/spf13/viper
* make batch migration worker tests less flaky
---------
Co-authored-by: Jesse Hallam <jesse@mattermost.com>
* (a11y-test): team menu (#33998)
* MM-66071: Do not error on empty slice in /groups/names (#34021)
* Do not error on empty slice in /groups/names
If group_names is an empty slice, this should not be an invalid
parameter. We return what we were asked for: an empty array.
* Avoid requests to /groups/names if list is empty
If the list of group names is empty, we do not need to ask the server
for the corresponding groups: we already know it'll be an empty list.
* Fix ABAC not available for entry (#34027)
Automatic Merge
* Explicitly name Postgres container volume (#33954)
* Explicitly name Postgres container volume
* Remove unused server/docker-compose.yaml
This file doesn't seem to actually be used. When we run docker compose locally,
it uses docker-compose.makefile.yml merged with the output of
build/docker-compose-generator/main.go.
* Revert "Remove unused server/docker-compose.yaml"
This reverts commit 5a45965217.
* Update volume name
* Flag post API (#33765)
* Added enable/disable setting and feature flag
* added rest of notifgication settings
* Added backend for content flagging setting and populated notification values from server side defaults
* WIP user selector
* Added common reviewers UI
* Added additonal reviewers section
* WIP
* WIP
* Team table base
* Added search in teams
* Added search in teams
* Added additional settings section
* WIP
* Inbtegrated reviewers settings
* WIP
* WIP
* Added server side validation
* cleanup
* cleanup
* [skip ci]
* Some refactoring
* type fixes
* lint fix
* test: add content flagging settings test file
* test: add comprehensive unit tests for content flagging settings
* enhanced tests
* test: add test file for content flagging additional settings
* test: add comprehensive unit tests for ContentFlaggingAdditionalSettingsSection
* Added additoonal settings test
* test: add empty test file for team reviewers section
* test: add comprehensive unit tests for TeamReviewersSection component
* test: update tests to handle async data fetching in team reviewers section
* test: add empty test file for content reviewers component
* feat: add comprehensive unit tests for ContentFlaggingContentReviewers component
* Added ContentFlaggingContentReviewersContentFlaggingContentReviewers test
* test: add notification settings test file for content flagging
* test: add comprehensive unit tests for content flagging notification settings
* Added ContentFlaggingNotificationSettingsSection tests
* test: add user profile pill test file
* test: add comprehensive unit tests for UserProfilePill component
* refactor: Replace enzyme shallow with renderWithContext in user_profile_pill tests
* Added UserProfilePill tests
* test: add empty test file for content reviewers team option
* test: add comprehensive unit tests for TeamOptionComponent
* Added TeamOptionComponent tests
* test: add empty test file for reason_option component
* test: add comprehensive unit tests for ReasonOption component
* Added ReasonOption tests
* cleanup
* Fixed i18n error
* fixed e2e test lijnt issues
* Updated test cases
* Added snaoshot
* Updated snaoshot
* lint fix
* WIP
* lint fix
* Added post flagging properties setup
* review fixes
* updated snapshot
* CI
* Added base APIs
* Fetched team status data on load and team switch
* WIP
* Review fixes
* wip
* WIP
* Removed an test, updated comment
* CI
* Added tests
* Added tests
* Lint fix
* Added API specs
* Fixed types
* CI fixes
* API tests
* lint fixes
* Set env variable so API routes are regiustered
* Test update
* term renaming and disabling API tests on MySQL
* typo
* Updated store type definition
* Minor tweaks
* Added tests
* Removed error in app startup when content flaghging setup fails
* Updated sync condition:
* Flag message modal basE
* added post preview
* displaying options
* Adde comment input
* Updated tests and docs
* finction rename
* WIP
* Updated tests
* refactor
* lint fix
* MOved to data migration
* lint fix
* CI
* added new migration mocks
* Used setup for tests
* some comment
* Removed unnecesseery nil check
* Form validation
* WIP tests
* WIP tests
* WIP tests
* fix: mock content flagging config selector with correct reasons format
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>
* fix: add mock for getContentFlaggingConfig in flag post modal test
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>
* Updated error code order in API docs
* removed empty files
* Added tests
* lint fixes
* minor tweak
* lint fix
* type fix
* fixed test
* nit
* test enhancements
* API WIP
* API WIP
* creating values
* creating content flagging channel and properties
* Able to save properties
* Added another property field
* WIP
* WIP
* Added validations
* Added data validations and hidden post if confifgured to
* lint fixes
* Added API spec
* Added some tests
* Added tests for getContentReviewBot
* test: add comprehensive tests for getContentReviewChannels function
* Added more app layer tests
* Added TestCanFlagPost
* test: Add comprehensive tests for FlagPost function
* Added all app layer tests
* Removed a file that was reamoved downstream
* test: add content flagging test file
* test: add comprehensive tests for FlagContentRequest.IsValid method
* Added model tests
* test: add comprehensive tests for SqlPropertyValueStore.CreateMany
* test: add comprehensive tests for flagPost() API function
* Added API tests
* linter fix
* WIP
* sent post flagging confirmation message
* fixed i18n nissues
* fixed i18n nissues
* CI
* Updated test
* fix: reset contentFlaggingGroupId for test isolation in content flagging tests
* removed cached group ID
* removed debug log
* review fixes
* Used correct ot name
* CI
* Updated mobile text
* Handled JSON error
* fixerdf i18n
* CI
* Integrate flag post api (#33798)
* WIP
* WIP
* Added API call
* test: add test for Client4.flagPost API call in FlagPostModal
* fix: remove userEvent.setup() from flag post modal test
* test: wrap submit button click in act for proper state updates
* Updated tests
* lint fix
* CI
* Updated to allow special characters in comments
* Handled empty comment
* Used finally
* CI
* Fixed test
* Spillage card integration (#33832)
* Created getContentFlaggingFields API
* created getPostPropertyValues API
* WIP
* Created useContentFlaggingFields hook
* WIP
* WIP
* Added option to retain data for reviewers
* Displayed deleted post's preview
* DIsplayed all properties
* Adding field name i18n
* WIP - managing i18n able texts
* Finished displaying all fields
* Manual cleanup
* lint fixes
* team role filter logic fix
* Fixed tests
* created new API to fetch flagged posts
* lint fix
* Added new client methods
* test: add comprehensive tests for content flagging APIs
* Added new API tests
* fixed openapi spec
* Fixed DataSpillageReport tests
* Fixed PostMarkdown test
* Fixed PostPreviewPropertyRenderer test
* Added metadata to card renderer
* test fixes
* Added no comment placeholder
* Fixed test
* refactor: improve test mocking for data spillage report component
* test mock updates
* Updated reducer
* not resetting mocks
* WIP
* review fixes
* CI
* Fixed
* fixes
* Content flagging actions implementation (#33852)
* Added view detail button
* Created RemoveFlaggedMessageConfirmationModal modal
* Added key and remove flag request modal
* IMplemented delete flagged post
* Handled edge cases of deleting flagged post
* keep message
* UI integration
* Added WS event for post report update and handled deleted files of flagged post
* Added error handling in keep/remove forms
* i18n fixes
* Updated OpenAPI specs
* fixed types
* fixed types
* refactoring
* Fixed tests
* review fixes
* Added new property translations
* Improved test
* fixed test
* CI
* fixes
* CI
* fixed a test
* CI
---------
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>
* GH-34031: Add accessibility tests for Sidebar settings panel (#34041)
* Add accessibility tests for Sidebar settings panel
* Add aria snapshots and fix accessibility test expectations for sidebar settings
* Update system_user_detail to enable editing of User Attributes
- This adds a `updateUserCustomProfileAttributesValues` method to client4 to allow updating attributes for a given userID, as a match to getUserCustomProfilesAttributesValues
- Fixed issue in saveCustomProfileAttributes where it used the version that did not take the passed in userId
* Fixes for tests
* Update saveCustomProfileAttribute mock in users.test.ts
The `saveCustomProfileAttribute` test was still using the old getCustomProfileAttributeValuesRoute instead of the same route that `getCustomProfileAttributeValues` uses.
* Adding e2e tests for editing User attributes (custom and system) in System Console
* Fix testing issue with selector for 'Authentication Method'
* Fixes from PR suggestions
* Fix lint/export issues
* Fix playwright lint issue halting e2e smoke test
* Results from npm run prettier:fix (in e2e-tests/playwright folder)
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com>
Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
Co-authored-by: Pablo Vélez <pablovv2012@gmail.com>
Co-authored-by: Eva Sarafianou <eva.sarafianou@gmail.com>
Co-authored-by: Jesse Hallam <jesse@mattermost.com>
Co-authored-by: sabril <5334504+saturninoabril@users.noreply.github.com>
Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>
Co-authored-by: Mudit Sharma <ms1725@srmist.edu.in>
This commit is contained in:
parent
5a0dee5fc2
commit
df1c2920de
13 changed files with 1561 additions and 452 deletions
|
|
@ -69,7 +69,7 @@ describe('Settings', () => {
|
|||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'Email');
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'Email');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type saml user
|
||||
|
|
@ -77,7 +77,7 @@ describe('Settings', () => {
|
|||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'SAML');
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'SAML');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type ldap user
|
||||
|
|
@ -85,7 +85,7 @@ describe('Settings', () => {
|
|||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'LDAP');
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'LDAP');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type mfa user
|
||||
|
|
@ -93,7 +93,7 @@ describe('Settings', () => {
|
|||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('contain.text', 'MFA');
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('contain.text', 'MFA');
|
||||
cy.get('a.back').click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ export async function setupCustomProfileAttributeValues(
|
|||
fields: Record<string, UserPropertyField>,
|
||||
): Promise<void> {
|
||||
// Create a map of attribute values by field ID
|
||||
const valuesByFieldId: Record<string, string> = {};
|
||||
const valuesByFieldId: Record<string, string | string[]> = {};
|
||||
|
||||
for (const attr of attributes) {
|
||||
let fieldID = '';
|
||||
|
|
@ -407,6 +407,51 @@ export async function setupCustomProfileAttributeValues(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up custom profile attribute values for a specific user (admin function)
|
||||
* @param {Client4} adminClient - Admin client object
|
||||
* @param {Array} attributes - Array of attribute objects with name and value
|
||||
* @param {Object} fields - Map of field IDs to field objects
|
||||
* @param {string} targetUserId - ID of the user to set values for
|
||||
*/
|
||||
export async function setupCustomProfileAttributeValuesForUser(
|
||||
adminClient: Client4,
|
||||
attributes: CustomProfileAttribute[],
|
||||
fields: Record<string, UserPropertyField>,
|
||||
targetUserId: string,
|
||||
): Promise<void> {
|
||||
// Create a map of attribute values by field ID
|
||||
const valuesByFieldId: Record<string, string | string[]> = {};
|
||||
|
||||
for (const attr of attributes) {
|
||||
let fieldID = '';
|
||||
|
||||
// Find the field ID for this attribute name
|
||||
for (const [id, field] of Object.entries(fields)) {
|
||||
if (field.name === attr.name) {
|
||||
fieldID = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a matching field, add it to our values object
|
||||
if (fieldID && attr.value) {
|
||||
valuesByFieldId[fieldID] = attr.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Only make the API call if we have values to set
|
||||
if (Object.keys(valuesByFieldId).length > 0) {
|
||||
try {
|
||||
// Use the admin client method for updating other user's values
|
||||
await adminClient.updateUserCustomProfileAttributesValues(targetUserId, valuesByFieldId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Failed to set attribute values for user:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all custom profile attributes
|
||||
* @param {Client4} adminClient - Admin API client
|
||||
|
|
|
|||
|
|
@ -0,0 +1,429 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* LOCALIZATION NOTE:
|
||||
* This test suite uses test attributes (data-testid, id) for buttons to avoid
|
||||
* language dependencies, but still has some localization dependencies:
|
||||
* - Error message text assertions (e.g., 'Invalid email address')
|
||||
* - Field labels (e.g., 'Username', 'Email')
|
||||
* - Test data values (e.g., 'JavaScript', 'Python' option names)
|
||||
*
|
||||
* Currently runs in English-only (locale: 'en-US' in playwright.config.ts).
|
||||
* For multi-language support, these assertions would need refactoring.
|
||||
*/
|
||||
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {Client4} from '@mattermost/client';
|
||||
import {UserPropertyField} from '@mattermost/types/properties';
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
CustomProfileAttribute,
|
||||
setupCustomProfileAttributeFields,
|
||||
setupCustomProfileAttributeValuesForUser,
|
||||
deleteCustomProfileAttributes,
|
||||
} from '../../channels/custom_profile_attributes/helpers';
|
||||
|
||||
// Test data for different user attribute types (non-synced only)
|
||||
const testUserAttributes: CustomProfileAttribute[] = [
|
||||
{
|
||||
name: 'Department',
|
||||
value: 'Engineering',
|
||||
type: 'text',
|
||||
attrs: {
|
||||
visibility: 'when_set', // Ensure it's not synced
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Work Email',
|
||||
value: 'work@company.com',
|
||||
type: 'text',
|
||||
attrs: {
|
||||
value_type: 'email',
|
||||
visibility: 'when_set', // Ensure it's not synced
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Personal Website',
|
||||
value: 'https://johndoe.com',
|
||||
type: 'text',
|
||||
attrs: {
|
||||
value_type: 'url',
|
||||
visibility: 'when_set', // Ensure it's not synced
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Location',
|
||||
type: 'select',
|
||||
attrs: {
|
||||
visibility: 'when_set', // Ensure it's not synced
|
||||
},
|
||||
options: [
|
||||
{name: 'Remote', color: '#00FFFF'},
|
||||
{name: 'Office', color: '#FF00FF'},
|
||||
{name: 'Hybrid', color: '#FFFF00'},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Skills',
|
||||
type: 'multiselect',
|
||||
attrs: {
|
||||
visibility: 'when_set', // Ensure it's not synced
|
||||
},
|
||||
options: [
|
||||
{name: 'JavaScript', color: '#F0DB4F'},
|
||||
{name: 'React', color: '#61DAFB'},
|
||||
{name: 'Python', color: '#3776AB'},
|
||||
{name: 'Go', color: '#00ADD8'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let team: Team;
|
||||
let adminUser: UserProfile;
|
||||
let testUser: UserProfile;
|
||||
let attributeFieldsMap: Record<string, UserPropertyField>;
|
||||
let adminClient: Client4;
|
||||
let systemConsolePage: any;
|
||||
|
||||
test.describe('System Console - Admin User Profile Editing', () => {
|
||||
test.beforeEach(async ({pw}) => {
|
||||
// Ensure license for Custom Profile Attributes functionality
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// Initialize with admin client
|
||||
({team, adminUser, adminClient} = await pw.initSetup());
|
||||
|
||||
// Create test user to edit
|
||||
testUser = await pw.createNewUserProfile(adminClient, 'admin-edit-target-');
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
// Set up custom user attribute fields
|
||||
attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, testUserAttributes);
|
||||
|
||||
// Set initial custom attribute values for the test user
|
||||
await setupCustomProfileAttributeValuesForUser(
|
||||
adminClient,
|
||||
testUserAttributes,
|
||||
attributeFieldsMap,
|
||||
testUser.id,
|
||||
);
|
||||
|
||||
// Login as admin
|
||||
({systemConsolePage} = await pw.testBrowser.login(adminUser));
|
||||
|
||||
// Navigate to system console users
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
await systemConsolePage.sidebar.goToItem('Users');
|
||||
await systemConsolePage.systemUsers.toBeVisible();
|
||||
|
||||
// Search for target user and navigate to user detail page
|
||||
await systemConsolePage.systemUsers.enterSearchText(testUser.email);
|
||||
const userRow = await systemConsolePage.systemUsers.getNthRow(1);
|
||||
await userRow.getByText(testUser.email).click();
|
||||
|
||||
// Wait for user detail page to load
|
||||
await systemConsolePage.page.waitForURL(`**/admin_console/user_management/user/${testUser.id}`);
|
||||
});
|
||||
|
||||
test.afterEach(async ({pw}) => {
|
||||
// Clean up custom user attribute fields
|
||||
const {adminClient: cleanupClient} = await pw.getAdminClient();
|
||||
await deleteCustomProfileAttributes(cleanupClient, attributeFieldsMap);
|
||||
});
|
||||
|
||||
test('MM-65126 Should edit custom user attributes from system console', async () => {
|
||||
// # Find and edit Department field (custom text attribute) - look for input near Department label
|
||||
const departmentLabel = systemConsolePage.page.locator('label').filter({hasText: /Department/});
|
||||
const departmentInput = departmentLabel.locator('input').first();
|
||||
await departmentInput.clear();
|
||||
await departmentInput.fill('Marketing');
|
||||
|
||||
// # Click Save button (using test ID instead of text)
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify success (no error message and field retains new value)
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
await expect(departmentInput).toHaveValue('Marketing');
|
||||
|
||||
// * Verify Save button becomes disabled after successful save
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('Should display user attributes in two-column layout', async () => {
|
||||
// * Verify two-column layout exists
|
||||
const twoColumnLayout = systemConsolePage.page.locator('.two-column-layout');
|
||||
await expect(twoColumnLayout).toBeVisible();
|
||||
|
||||
// * Verify system fields are present (be more specific to avoid multiple matches)
|
||||
await expect(systemConsolePage.page.locator('label').filter({hasText: /^Username/})).toBeVisible();
|
||||
await expect(systemConsolePage.page.locator('label').filter({hasText: /^Authentication Method/})).toBeVisible();
|
||||
// Email field - check for system email (avoid Work Email by being more specific)
|
||||
const systemEmailExists = (await systemConsolePage.page.locator('input[type="email"]').count()) > 0;
|
||||
expect(systemEmailExists).toBe(true);
|
||||
|
||||
// * Verify custom user attributes are present
|
||||
for (const field of testUserAttributes) {
|
||||
await expect(
|
||||
systemConsolePage.page.locator('label').filter({hasText: new RegExp(field.name)}),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// * Verify we have input fields (at least 4-5 total)
|
||||
const inputElements = systemConsolePage.page.locator('input, select');
|
||||
const inputCount = await inputElements.count();
|
||||
expect(inputCount).toBeGreaterThan(4);
|
||||
|
||||
// * Verify fields are arranged in rows with two columns
|
||||
const fieldRows = systemConsolePage.page.locator('.field-row');
|
||||
const rowCount = await fieldRows.count();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Should edit system email attribute and save', async () => {
|
||||
// # Find system email field
|
||||
const systemEmailInput = systemConsolePage.page.locator('input[type="email"]').first();
|
||||
|
||||
// # Enter new valid email
|
||||
const newEmail = `updated-${testUser.email}`;
|
||||
await systemEmailInput.clear();
|
||||
await systemEmailInput.fill(newEmail);
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify success
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
await expect(systemEmailInput).toHaveValue(newEmail);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('Should edit custom select attribute and save', async () => {
|
||||
// # Find Location select field near its label
|
||||
const locationLabel = systemConsolePage.page.locator('label').filter({hasText: /Location/});
|
||||
const locationSelect = locationLabel.locator('select').first();
|
||||
|
||||
// # Get the first available option (since we can't predict the option value/ID)
|
||||
const firstOption = await locationSelect.locator('option').nth(1); // Skip the default "Select an option"
|
||||
const firstOptionValue = await firstOption.getAttribute('value');
|
||||
await locationSelect.selectOption(firstOptionValue || '');
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify success and persistence
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
// Don't check exact value since it's a generated ID, just verify it's not empty
|
||||
const selectedValue = await locationSelect.inputValue();
|
||||
expect(selectedValue).toBeTruthy();
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('Should display custom multiselect attribute and save form', async () => {
|
||||
// * Verify Skills multiselect component is displayed
|
||||
const skillsLabel = systemConsolePage.page.locator('label').filter({hasText: /Skills/});
|
||||
await expect(skillsLabel).toBeVisible();
|
||||
|
||||
// * Verify the multiselect control is present (React Select component)
|
||||
// Look for common React Select patterns
|
||||
const hasMultiselectElement =
|
||||
(await skillsLabel.locator('div, [class*="select"], [class*="Select"]').count()) > 0;
|
||||
expect(hasMultiselectElement).toBe(true);
|
||||
|
||||
// # Make a change to a different field to trigger save state
|
||||
const departmentLabel = systemConsolePage.page.locator('label').filter({hasText: /Department/});
|
||||
const departmentInput = departmentLabel.locator('input').first();
|
||||
await departmentInput.fill('Engineering Updated');
|
||||
|
||||
// # Verify save button becomes enabled
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
||||
// # Save the form
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify success (no error message)
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
|
||||
// * Verify save completed
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
// * Verify the change persisted
|
||||
await expect(departmentInput).toHaveValue('Engineering Updated');
|
||||
});
|
||||
|
||||
test('Should validate invalid email and show error with cancel option', async () => {
|
||||
// # Find system email field
|
||||
const systemEmailInput = systemConsolePage.page.locator('input[type="email"]').first();
|
||||
const originalEmail = await systemEmailInput.inputValue();
|
||||
|
||||
// # Enter invalid email
|
||||
await systemEmailInput.clear();
|
||||
await systemEmailInput.fill('not-an-email');
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify error message appears
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).toBeVisible();
|
||||
await expect(errorMessage).toContainText('Invalid email address');
|
||||
|
||||
// * Verify Cancel button is visible and enabled
|
||||
const cancelButton = systemConsolePage.page.locator('button:has-text("Cancel")');
|
||||
await expect(cancelButton).toBeVisible();
|
||||
await expect(cancelButton).toBeEnabled();
|
||||
|
||||
// * Verify Save button remains enabled (user can fix and retry)
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
||||
// # Test the cancel functionality
|
||||
await cancelButton.click();
|
||||
|
||||
// * Verify email reverts to original value
|
||||
await expect(systemEmailInput).toHaveValue(originalEmail);
|
||||
|
||||
// * Verify error message disappears
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
|
||||
// * Verify Cancel button disappears
|
||||
await expect(cancelButton).not.toBeVisible();
|
||||
|
||||
// * Verify Save button becomes disabled
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('Should validate invalid URL and show error with cancel option', async () => {
|
||||
// # Find custom URL field (Personal Website)
|
||||
const urlInput = systemConsolePage.page.locator('input[type="url"]').first();
|
||||
const originalUrl = await urlInput.inputValue();
|
||||
|
||||
// # Enter invalid URL (specifically the one mentioned: "<%>")
|
||||
await urlInput.clear();
|
||||
await urlInput.fill('<%>');
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify error message appears
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).toBeVisible();
|
||||
await expect(errorMessage).toContainText('Invalid URL');
|
||||
|
||||
// * Verify Cancel button is visible
|
||||
const cancelButton = systemConsolePage.page.locator('button:has-text("Cancel")');
|
||||
await expect(cancelButton).toBeVisible();
|
||||
await expect(cancelButton).toBeEnabled();
|
||||
|
||||
// # Test cancel functionality
|
||||
await cancelButton.click();
|
||||
|
||||
// * Verify URL reverts to original value
|
||||
await expect(urlInput).toHaveValue(originalUrl);
|
||||
|
||||
// * Verify error message disappears
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
|
||||
// * Verify Cancel button disappears
|
||||
await expect(cancelButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Should validate invalid email in custom email attribute', async () => {
|
||||
// # Find custom email field (Work Email) by its label
|
||||
const workEmailLabel = systemConsolePage.page.locator('label').filter({hasText: /Work Email/});
|
||||
const workEmailInput = workEmailLabel.locator('input[type="email"]').first();
|
||||
|
||||
// # Enter invalid email
|
||||
await workEmailInput.clear();
|
||||
await workEmailInput.fill('not-an-email-either');
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('button:has-text("Save")');
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify error message appears
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).toBeVisible();
|
||||
await expect(errorMessage).toContainText('Invalid email address');
|
||||
|
||||
// * Verify Cancel button is available
|
||||
const cancelButton = systemConsolePage.page.locator('button:has-text("Cancel")');
|
||||
await expect(cancelButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should show save/cancel buttons when changes are made', async () => {
|
||||
// * Initially, Save should be disabled and Cancel should not be visible
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
const cancelButton = systemConsolePage.page.locator('button:has-text("Cancel")');
|
||||
await expect(saveButton).toBeDisabled();
|
||||
await expect(cancelButton).not.toBeVisible();
|
||||
|
||||
// # Make a change to trigger save needed state - find Department field by label
|
||||
const departmentLabel = systemConsolePage.page.locator('label').filter({hasText: /Department/});
|
||||
const departmentInput = departmentLabel.locator('input').first();
|
||||
const originalValue = await departmentInput.inputValue();
|
||||
await departmentInput.clear();
|
||||
await departmentInput.fill('Changed Value');
|
||||
|
||||
// * Verify Save button becomes enabled and Cancel button appears
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await expect(cancelButton).toBeVisible();
|
||||
await expect(cancelButton).toBeEnabled();
|
||||
|
||||
// # Click Cancel
|
||||
await cancelButton.click();
|
||||
|
||||
// * Verify changes are reverted
|
||||
await expect(departmentInput).toHaveValue(originalValue);
|
||||
|
||||
// * Verify Cancel button disappears
|
||||
await expect(cancelButton).not.toBeVisible();
|
||||
|
||||
// * Verify Save button is disabled
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('Should save all user attribute changes atomically', async () => {
|
||||
// # Make changes to both system and custom attributes
|
||||
const systemEmailInput = systemConsolePage.page.locator('input[type="email"]').first();
|
||||
const newEmail = `atomic-test-${testUser.email}`;
|
||||
await systemEmailInput.clear();
|
||||
await systemEmailInput.fill(newEmail);
|
||||
|
||||
const departmentLabel = systemConsolePage.page.locator('label').filter({hasText: /Department/});
|
||||
const departmentInput = departmentLabel.locator('input').first();
|
||||
await departmentInput.clear();
|
||||
await departmentInput.fill('Sales');
|
||||
|
||||
// # Click Save button
|
||||
const saveButton = systemConsolePage.page.locator('[data-testid="saveSetting"]');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await saveButton.click();
|
||||
|
||||
// * Verify both changes were saved successfully
|
||||
const errorMessage = systemConsolePage.page.locator('.error-message');
|
||||
await expect(errorMessage).not.toBeVisible();
|
||||
await expect(systemEmailInput).toHaveValue(newEmail);
|
||||
await expect(departmentInput).toHaveValue('Sales');
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -67,62 +67,77 @@ exports[`SystemUserDetail should match default snapshot 1`] = `
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -190,17 +205,21 @@ exports[`SystemUserDetail should match default snapshot 1`] = `
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
@ -276,62 +295,77 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = `
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -399,17 +433,21 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = `
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
@ -485,62 +523,77 @@ exports[`SystemUserDetail should not show manage user settings button when user
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -608,17 +661,21 @@ exports[`SystemUserDetail should not show manage user settings button when user
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
@ -694,62 +751,77 @@ exports[`SystemUserDetail should show manage user settings button as activated 1
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -823,17 +895,21 @@ exports[`SystemUserDetail should show manage user settings button as activated 1
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
@ -909,62 +985,77 @@ exports[`SystemUserDetail should show manage user settings button as disabled wh
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Email
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -1032,17 +1123,21 @@ exports[`SystemUserDetail should show manage user settings button as disabled wh
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
@ -1118,62 +1213,77 @@ exports[`SystemUserDetail should show the activate user button as disabled when
|
|||
class="AdminUserCard__body"
|
||||
>
|
||||
<span />
|
||||
<label>
|
||||
Email
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<div
|
||||
class="two-column-layout"
|
||||
>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Username
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
>
|
||||
<path
|
||||
d="M12,15C12.81,15 13.5,14.7 14.11,14.11C14.7,13.5 15,12.81 15,12C15,11.19 14.7,10.5 14.11,9.89C13.5,9.3 12.81,9 12,9C11.19,9 10.5,9.3 9.89,9.89C9.3,10.5 9,11.19 9,12C9,12.81 9.3,13.5 9.89,14.11C10.5,14.7 11.19,15 12,15M12,2C14.75,2 17.1,3 19.05,4.95C21,6.9 22,9.25 22,12V13.45C22,14.45 21.65,15.3 21,16C20.3,16.67 19.5,17 18.5,17C17.3,17 16.31,16.5 15.56,15.5C14.56,16.5 13.38,17 12,17C10.63,17 9.45,16.5 8.46,15.54C7.5,14.55 7,13.38 7,12C7,10.63 7.5,9.45 8.46,8.46C9.45,7.5 10.63,7 12,7C13.38,7 14.55,7.5 15.54,8.46C16.5,9.45 17,10.63 17,12V13.45C17,13.86 17.16,14.22 17.46,14.53C17.76,14.84 18.11,15 18.5,15C18.92,15 19.27,14.84 19.57,14.53C19.87,14.22 20,13.86 20,13.45V12C20,9.81 19.23,7.93 17.65,6.35C16.07,4.77 14.19,4 12,4C9.81,4 7.93,4.77 6.35,6.35C4.77,7.93 4,9.81 4,12C4,14.19 4.77,16.07 6.35,17.65C7.93,19.23 9.81,20 12,20H17V22H12C9.25,22 6.9,21 4.95,19.05C3,17.1 2,14.75 2,12C2,9.25 3,6.9 4.95,4.95C6.9,3 9.25,2 12,2Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
some-user
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
<label>
|
||||
Authentication Method
|
||||
<span>
|
||||
<svg
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
LDAP
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="field-row"
|
||||
>
|
||||
<div
|
||||
class="field-column left"
|
||||
>
|
||||
<path
|
||||
d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
LDAP
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="field-column right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AdminUserCard__footer"
|
||||
|
|
@ -1244,17 +1354,21 @@ exports[`SystemUserDetail should show the activate user button as disabled when
|
|||
<div
|
||||
class="admin-console-save"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
<div
|
||||
class="admin-console-save-buttons"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="saveSetting"
|
||||
disabled=""
|
||||
id="saveSetting"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="error-message"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import {connect} from 'react-redux';
|
|||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
|
||||
import {getUserPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {addUserToTeam} from 'mattermost-redux/actions/teams';
|
||||
import {updateUserActive, getUser, patchUser, updateUserMfa} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {updateUserActive, getUser, patchUser, updateUserMfa, getCustomProfileAttributeValues, saveCustomProfileAttribute} from 'mattermost-redux/actions/users';
|
||||
import {getConfig, getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
|
@ -19,12 +20,14 @@ import SystemUserDetail from './system_user_detail';
|
|||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const customProfileAttributeFields = Object.values(getCustomProfileAttributes(state));
|
||||
|
||||
const showManageUserSettings = getShowManageUserSettings(state);
|
||||
const showLockedManageUserSettings = getShowLockedManageUserSettings(state);
|
||||
|
||||
return {
|
||||
mfaEnabled: config?.EnableMultifactorAuthentication === 'true' || false,
|
||||
customProfileAttributeFields,
|
||||
showManageUserSettings,
|
||||
showLockedManageUserSettings,
|
||||
};
|
||||
|
|
@ -39,6 +42,9 @@ const mapDispatchToProps = {
|
|||
setNavigationBlocked,
|
||||
openModal,
|
||||
getUserPreferences,
|
||||
getCustomProfileAttributeFields,
|
||||
getCustomProfileAttributeValues,
|
||||
saveCustomProfileAttribute,
|
||||
};
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,26 @@
|
|||
.SystemUserDetail {
|
||||
.AdminUserCard__body {
|
||||
.two-column-layout {
|
||||
.field-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
gap: 20px;
|
||||
|
||||
.field-column {
|
||||
min-width: 0; // Allow content to shrink
|
||||
flex: 1;
|
||||
|
||||
&.left {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&.right {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: unset;
|
||||
|
|
@ -16,10 +37,40 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input {
|
||||
input, select {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.cpa-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-property-field-values__sync-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.7);
|
||||
font-size: 11px;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 6px;
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-data-field {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.1);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
@ -44,4 +95,24 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile responsiveness for two-column layout
|
||||
@media (max-width: 768px) {
|
||||
.AdminUserCard__body {
|
||||
.two-column-layout {
|
||||
.field-row {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
|
||||
.field-column {
|
||||
padding: 0;
|
||||
|
||||
&.right {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ describe('SystemUserDetail', () => {
|
|||
showManageUserSettings: false,
|
||||
showLockedManageUserSettings: false,
|
||||
mfaEnabled: false,
|
||||
customProfileAttributeFields: [],
|
||||
patchUser: jest.fn(),
|
||||
updateUserMfa: jest.fn(),
|
||||
getUser: getUserMock,
|
||||
|
|
@ -38,6 +39,9 @@ describe('SystemUserDetail', () => {
|
|||
addUserToTeam: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
getUserPreferences: jest.fn(),
|
||||
getCustomProfileAttributeFields: jest.fn().mockResolvedValue({data: []}),
|
||||
getCustomProfileAttributeValues: jest.fn().mockResolvedValue({data: {}}),
|
||||
saveCustomProfileAttribute: jest.fn().mockResolvedValue({data: {}}),
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
} as MockIntl,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {PureComponent} from 'react';
|
||||
import type {ChangeEvent, MouseEvent} from 'react';
|
||||
import type {IntlShape, WrappedComponentProps} from 'react-intl';
|
||||
import {FormattedMessage, defineMessage, injectIntl} from 'react-intl';
|
||||
import type {RouteComponentProps} from 'react-router-dom';
|
||||
import ReactSelect from 'react-select';
|
||||
|
||||
import {SyncIcon} from '@mattermost/compass-icons/components';
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserPropertyField} from '@mattermost/types/properties';
|
||||
import type {Team, TeamMembership} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
import {isEmail, getInputTypeFromValueType} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import AdminUserCard from 'components/admin_console/admin_user_card/admin_user_card';
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
|
|
@ -28,18 +33,71 @@ import UserSettingsModal from 'components/user_settings/modal';
|
|||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
import AdminPanel from 'components/widgets/admin_console/admin_panel';
|
||||
import AtIcon from 'components/widgets/icons/at_icon';
|
||||
import EmailIcon from 'components/widgets/icons/email_icon';
|
||||
import SheidOutlineIcon from 'components/widgets/icons/shield_outline_icon';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {validHttpUrl} from 'utils/url';
|
||||
import {toTitleCase} from 'utils/utils';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
import './system_user_detail.scss';
|
||||
|
||||
// Private component for CPA multiselect fields
|
||||
type CPAMultiSelectProps = {
|
||||
options: Array<{id: string; name: string}>;
|
||||
selectedValues: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
disabled: boolean;
|
||||
placeholder: string;
|
||||
noOptionsMessage: string;
|
||||
};
|
||||
|
||||
const CPAMultiSelect: React.FC<CPAMultiSelectProps> = ({
|
||||
options,
|
||||
selectedValues,
|
||||
onChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
noOptionsMessage,
|
||||
}) => {
|
||||
// Transform options to ReactSelect format
|
||||
const selectOptions = options.map((option) => ({
|
||||
value: option.id,
|
||||
label: option.name,
|
||||
}));
|
||||
|
||||
// Transform selected values to ReactSelect format
|
||||
const selectedOptions = selectedValues.map((selectedId) => {
|
||||
const option = options.find((opt) => opt.id === selectedId);
|
||||
return option ? {value: option.id, label: option.name} : null;
|
||||
}).filter((opt): opt is {value: string; label: string} => opt !== null);
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
isMulti={true}
|
||||
options={selectOptions}
|
||||
value={selectedOptions}
|
||||
onChange={(selectedOptions) => {
|
||||
const selectedIds = selectedOptions ? selectedOptions.map((opt) => opt.value) : [];
|
||||
onChange(selectedIds);
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
isClearable={false}
|
||||
placeholder={placeholder}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
styles={{
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
maxWidth: '320px',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type Params = {
|
||||
user_id?: UserProfile['id'];
|
||||
};
|
||||
|
|
@ -49,6 +107,9 @@ export type Props = PropsFromRedux & RouteComponentProps<Params> & WrappedCompon
|
|||
export type State = {
|
||||
user?: UserProfile;
|
||||
emailField: string;
|
||||
customProfileAttributeFields: UserPropertyField[];
|
||||
customProfileAttributeValues: Record<string, string | string[]>;
|
||||
originalCpaValues: Record<string, string | string[]>;
|
||||
isLoading: boolean;
|
||||
error?: string | null;
|
||||
isSaveNeeded: boolean;
|
||||
|
|
@ -66,6 +127,9 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
super(props);
|
||||
this.state = {
|
||||
emailField: '',
|
||||
customProfileAttributeFields: [],
|
||||
customProfileAttributeValues: {},
|
||||
originalCpaValues: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isSaveNeeded: false,
|
||||
|
|
@ -83,15 +147,23 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
this.setState({isLoading: true});
|
||||
|
||||
try {
|
||||
const {data, error} = await this.props.getUser(userId) as ActionResult<UserProfile, ServerError>;
|
||||
if (data) {
|
||||
// Fetch user data and CPA values in parallel
|
||||
const [userResult, cpaResult] = await Promise.all([
|
||||
this.props.getUser(userId) as ActionResult<UserProfile, ServerError>,
|
||||
this.props.getCustomProfileAttributeValues(userId),
|
||||
]);
|
||||
|
||||
if (userResult.data) {
|
||||
const cpaValues = (cpaResult as {data?: Record<string, string | string[]>}).data || {};
|
||||
this.setState({
|
||||
user: data,
|
||||
emailField: data.email, // Set emailField to the email of the user for editing purposes
|
||||
user: userResult.data,
|
||||
emailField: userResult.data.email, // Set emailField to the email of the user for editing purposes
|
||||
customProfileAttributeValues: cpaValues,
|
||||
originalCpaValues: {...cpaValues}, // Deep copy for change tracking
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error ? error.message : 'Unknown error');
|
||||
throw new Error(userResult.error ? userResult.error.message : this.props.intl.formatMessage({id: 'admin.user_item.unknownError', defaultMessage: 'Unknown error'}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('SystemUserDetails-getUser: ', error); // eslint-disable-line no-console
|
||||
|
|
@ -109,6 +181,11 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
// We dont have to handle the case of userId being empty here because the redirect will take care of it from the parent components
|
||||
this.getUser(userId);
|
||||
}
|
||||
|
||||
// Fetch CPA field definitions if not already available
|
||||
if (this.props.customProfileAttributeFields.length === 0) {
|
||||
this.props.getCustomProfileAttributeFields();
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamsLoaded = (teams: TeamMembership[]) => {
|
||||
|
|
@ -202,13 +279,257 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
|
||||
const {target: {value}} = event;
|
||||
|
||||
const didEmailChanged = value !== this.state.user.email;
|
||||
this.setState({
|
||||
emailField: value,
|
||||
isSaveNeeded: didEmailChanged,
|
||||
error: null, // Clear any validation errors when user starts editing
|
||||
}, () => {
|
||||
this.checkForChanges();
|
||||
});
|
||||
};
|
||||
|
||||
handleCpaValueChange = (fieldId: string, value: string | string[]) => {
|
||||
this.setState({
|
||||
customProfileAttributeValues: {
|
||||
...this.state.customProfileAttributeValues,
|
||||
[fieldId]: value,
|
||||
},
|
||||
error: null, // Clear any validation errors when user starts editing
|
||||
}, () => {
|
||||
this.checkForChanges();
|
||||
});
|
||||
};
|
||||
|
||||
checkForChanges = () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailChanged = this.state.emailField !== this.state.user.email;
|
||||
const cpaChanged = this.hasCpaChanges();
|
||||
const hasChanges = emailChanged || cpaChanged;
|
||||
|
||||
this.setState({
|
||||
isSaveNeeded: hasChanges,
|
||||
});
|
||||
|
||||
this.props.setNavigationBlocked(didEmailChanged);
|
||||
this.props.setNavigationBlocked(hasChanges);
|
||||
};
|
||||
|
||||
hasCpaChanges = (): boolean => {
|
||||
const {customProfileAttributeValues, originalCpaValues} = this.state;
|
||||
|
||||
// Check if any CPA value has changed
|
||||
const currentFields = new Set([...Object.keys(customProfileAttributeValues), ...Object.keys(originalCpaValues)]);
|
||||
|
||||
for (const fieldId of currentFields) {
|
||||
const currentValue = customProfileAttributeValues[fieldId];
|
||||
const originalValue = originalCpaValues[fieldId];
|
||||
|
||||
// Handle array comparison for multiselect fields
|
||||
if (Array.isArray(currentValue) && Array.isArray(originalValue)) {
|
||||
if (currentValue.length !== originalValue.length ||
|
||||
currentValue.some((val, idx) => val !== originalValue[idx])) {
|
||||
return true;
|
||||
}
|
||||
} else if (currentValue !== originalValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
renderCpaField = (field: UserPropertyField) => {
|
||||
const value = this.state.customProfileAttributeValues[field.id] || '';
|
||||
const isSynced = Boolean(field.attrs?.ldap || field.attrs?.saml);
|
||||
const isDisabled = this.state.isSaving || this.state.isLoading || isSynced;
|
||||
|
||||
// Render sync indicator if field is synced
|
||||
const syncIndicator = isSynced ? (
|
||||
<div className='user-property-field-values__sync-indicator'>
|
||||
<SyncIcon size={18}/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.syncedWith'
|
||||
defaultMessage='Synced with: {source}'
|
||||
values={{
|
||||
source: field.attrs?.ldap ? this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.ldap', defaultMessage: 'AD/LDAP: {propertyName}'}, {propertyName: field.attrs.ldap}) : this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.saml', defaultMessage: 'SAML: {propertyName}'}, {propertyName: field.attrs?.saml}),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const fieldContent = (() => {
|
||||
switch (field.type) {
|
||||
case 'select': {
|
||||
const options = field.attrs?.options || [];
|
||||
return (
|
||||
<select
|
||||
className='form-control'
|
||||
value={Array.isArray(value) ? value[0] || '' : value}
|
||||
onChange={(e) => this.handleCpaValueChange(field.id, e.target.value)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<option value=''>
|
||||
{this.props.intl.formatMessage({
|
||||
id: 'admin.userManagement.userDetail.selectOption',
|
||||
defaultMessage: 'Select an option',
|
||||
})}
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
case 'multiselect': {
|
||||
const options = field.attrs?.options || [];
|
||||
const selectedValues = Array.isArray(value) ? value : [];
|
||||
|
||||
return (
|
||||
<CPAMultiSelect
|
||||
options={options}
|
||||
selectedValues={selectedValues}
|
||||
onChange={(values) => this.handleCpaValueChange(field.id, values)}
|
||||
disabled={isDisabled}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'admin.user.selectOptions',
|
||||
defaultMessage: 'Select options...',
|
||||
})}
|
||||
noOptionsMessage={this.props.intl.formatMessage({
|
||||
id: 'admin.userManagement.userDetail.noOptions',
|
||||
defaultMessage: 'No options available',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'text':
|
||||
default: {
|
||||
const inputType = getInputTypeFromValueType(field.attrs?.value_type);
|
||||
|
||||
return (
|
||||
<input
|
||||
className='form-control'
|
||||
type={inputType}
|
||||
value={Array.isArray(value) ? value.join(this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.arrayValueSeparator', defaultMessage: ', '})) : value}
|
||||
onChange={(e) => this.handleCpaValueChange(field.id, e.target.value)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<label
|
||||
key={field.id}
|
||||
className='cpa-field'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.cpaField'
|
||||
defaultMessage='{fieldName}'
|
||||
values={{fieldName: field.name}}
|
||||
/>
|
||||
{fieldContent}
|
||||
{syncIndicator}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
renderTwoColumnLayout = () => {
|
||||
const sortedCpaFields = [...this.props.customProfileAttributeFields].
|
||||
sort((a, b) => (a.attrs?.sort_order || 0) - (b.attrs?.sort_order || 0));
|
||||
|
||||
const fields: Array<React.ReactNode | null> = [];
|
||||
|
||||
// Add system fields
|
||||
fields.push(
|
||||
<label key='username'>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.username'
|
||||
defaultMessage='Username'
|
||||
/>
|
||||
<AtIcon/>
|
||||
<span>{this.state.user?.username}</span>
|
||||
</label>,
|
||||
);
|
||||
|
||||
fields.push(
|
||||
<label key='authMethod'>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.authenticationMethod'
|
||||
defaultMessage='Authentication Method'
|
||||
/>
|
||||
<SheidOutlineIcon/>
|
||||
<span>{getUserAuthenticationTextField(this.props.intl, this.props.mfaEnabled, this.state.user)}</span>
|
||||
</label>,
|
||||
);
|
||||
|
||||
fields.push(
|
||||
<label key='email'>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.email'
|
||||
defaultMessage='Email'
|
||||
/>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.emailField}
|
||||
onChange={this.handleEmailChange}
|
||||
disabled={this.state.isSaving || this.state.isLoading}
|
||||
/>
|
||||
</label>,
|
||||
);
|
||||
|
||||
// Add CPA fields
|
||||
for (const field of sortedCpaFields) {
|
||||
fields.push(this.renderCpaField(field));
|
||||
}
|
||||
|
||||
// Pad for even number
|
||||
if (fields.length % 2) {
|
||||
fields.push(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='two-column-layout'>
|
||||
{fields.map((field, index) => {
|
||||
if (index % 2 === 0) { // Start of new row
|
||||
return (
|
||||
<div
|
||||
key={`field-row-${Math.trunc(index / 2)}`}
|
||||
className='field-row'
|
||||
>
|
||||
<div className='field-column left'>
|
||||
{field}
|
||||
</div>
|
||||
<div className='field-column right'>
|
||||
{fields[index + 1]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null; // Skip odd indices
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleCancel = () => {
|
||||
// Reset all fields to original values
|
||||
this.setState({
|
||||
emailField: this.state.user?.email || '',
|
||||
customProfileAttributeValues: {...this.state.originalCpaValues},
|
||||
error: null,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
this.props.setNavigationBlocked(false);
|
||||
};
|
||||
|
||||
handleSubmit = async (event: MouseEvent<HTMLButtonElement>) => {
|
||||
|
|
@ -218,16 +539,43 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.state.user.email === this.state.emailField) {
|
||||
if (!this.state.isSaveNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmail(this.state.user.email)) {
|
||||
// Validate email if changed
|
||||
const emailChanged = this.state.user.email !== this.state.emailField;
|
||||
if (emailChanged && !isEmail(this.state.emailField)) {
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.invalidEmail', defaultMessage: 'Invalid email address'})});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUser = Object.assign({}, this.state.user, {email: this.state.emailField.trim().toLowerCase()});
|
||||
// Validate CPA values if changed
|
||||
const cpaChanged = this.hasCpaChanges();
|
||||
if (cpaChanged) {
|
||||
const {customProfileAttributeFields} = this.props;
|
||||
for (const field of customProfileAttributeFields) {
|
||||
const valueType = field.attrs?.value_type;
|
||||
const currentValue = this.state.customProfileAttributeValues[field.id];
|
||||
const originalValue = this.state.originalCpaValues[field.id];
|
||||
if (!currentValue || !valueType || currentValue === originalValue) {
|
||||
continue;
|
||||
}
|
||||
if (valueType === 'email') {
|
||||
const stringValue = String(currentValue);
|
||||
if (!isEmail(stringValue)) {
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.invalidEmail', defaultMessage: 'Invalid email address'})});
|
||||
return;
|
||||
}
|
||||
} else if (valueType === 'url') {
|
||||
const stringValue = String(currentValue);
|
||||
if (validHttpUrl(stringValue) === null) {
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.invalidUrl', defaultMessage: 'Invalid URL'})});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
|
|
@ -235,17 +583,79 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
});
|
||||
|
||||
try {
|
||||
const {data, error} = await this.props.patchUser(updatedUser) as ActionResult<UserProfile, ServerError>;
|
||||
if (data) {
|
||||
this.setState({
|
||||
user: data,
|
||||
emailField: data.email,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error ? error.message : 'Unknown error');
|
||||
const promises = [];
|
||||
|
||||
// Update user profile if email changed
|
||||
if (emailChanged) {
|
||||
const updatedUser = Object.assign({}, this.state.user, {email: this.state.emailField.trim().toLowerCase()});
|
||||
promises.push(this.props.patchUser(updatedUser));
|
||||
}
|
||||
|
||||
// Update CPA values if changed
|
||||
if (cpaChanged) {
|
||||
// Get only changed CPA values and save each one using Redux action
|
||||
const {customProfileAttributeFields} = this.props;
|
||||
for (const field of customProfileAttributeFields) {
|
||||
const currentValue = this.state.customProfileAttributeValues[field.id];
|
||||
const originalValue = this.state.originalCpaValues[field.id];
|
||||
|
||||
// Check if this field value has changed
|
||||
let hasChanged = false;
|
||||
if (Array.isArray(currentValue) && Array.isArray(originalValue)) {
|
||||
hasChanged = currentValue.length !== originalValue.length ||
|
||||
currentValue.some((val, idx) => val !== originalValue[idx]);
|
||||
} else {
|
||||
hasChanged = currentValue !== originalValue;
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
promises.push(this.props.saveCustomProfileAttribute(this.state.user!.id, field.id, currentValue || ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all updates in parallel
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Handle results
|
||||
let updatedUser = this.state.user;
|
||||
let resultIndex = 0;
|
||||
|
||||
// Handle user update result if email was changed
|
||||
if (emailChanged) {
|
||||
const userResult = results[resultIndex] as ActionResult<UserProfile, ServerError>;
|
||||
if (userResult.data) {
|
||||
updatedUser = userResult.data;
|
||||
} else if (userResult.error) {
|
||||
throw new Error(userResult.error.message);
|
||||
}
|
||||
resultIndex++;
|
||||
}
|
||||
|
||||
// Handle CPA update results if CPA values were changed
|
||||
if (cpaChanged) {
|
||||
// Check remaining results for any CPA save errors
|
||||
for (let i = resultIndex; i < results.length; i++) {
|
||||
const cpaResult = results[i] as ActionResult<Record<string, string | string[]>, ServerError>;
|
||||
if (cpaResult.error) {
|
||||
throw new Error(cpaResult.error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update state with successful results
|
||||
this.setState({
|
||||
user: updatedUser,
|
||||
emailField: updatedUser.email,
|
||||
originalCpaValues: {...this.state.customProfileAttributeValues}, // Update original values
|
||||
error: null,
|
||||
isSaving: false,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
|
||||
// Refresh user data to ensure we have latest CPA values from server
|
||||
if (cpaChanged) {
|
||||
await this.props.getCustomProfileAttributeValues(this.state.user.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SystemUserDetails-handleSubmit', err); // eslint-disable-line no-console
|
||||
|
|
@ -253,8 +663,8 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
this.setState({
|
||||
error: this.props.intl.formatMessage({id: 'admin.user_item.userUpdateFailed', defaultMessage: 'Failed to update user'}),
|
||||
isSaving: false,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
return; // Don't unblock navigation on error
|
||||
}
|
||||
|
||||
this.props.setNavigationBlocked(false);
|
||||
|
|
@ -368,38 +778,9 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
body={
|
||||
<>
|
||||
<span>{this.state.user?.position ?? ''}</span>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.email'
|
||||
defaultMessage='Email'
|
||||
/>
|
||||
<EmailIcon/>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.emailField}
|
||||
onChange={this.handleEmailChange}
|
||||
disabled={this.state.error !== null || this.state.isSaving}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.username'
|
||||
defaultMessage='Username'
|
||||
/>
|
||||
<AtIcon/>
|
||||
<span>{this.state.user?.username}</span>
|
||||
</label>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.authenticationMethod'
|
||||
defaultMessage='Authentication Method'
|
||||
/>
|
||||
<SheidOutlineIcon/>
|
||||
<span>{getUserAuthenticationTextField(this.props.intl, this.props.mfaEnabled, this.state.user)}</span>
|
||||
</label>
|
||||
{this.renderTwoColumnLayout()}
|
||||
{Boolean(this.state.user?.auth_data && this.state.user?.auth_service) && (
|
||||
<label>
|
||||
<label className='auth-data-field'>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.authData'
|
||||
defaultMessage='Auth Data'
|
||||
|
|
@ -546,11 +927,27 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
|||
|
||||
{/* Footer */}
|
||||
<div className='admin-console-save'>
|
||||
<SaveButton
|
||||
saving={this.state.isSaving}
|
||||
disabled={!this.state.isSaveNeeded || this.state.isLoading || this.state.error !== null || this.state.isSaving}
|
||||
onClick={this.handleSubmit}
|
||||
/>
|
||||
<div className='admin-console-save-buttons'>
|
||||
<SaveButton
|
||||
saving={this.state.isSaving}
|
||||
disabled={!this.state.isSaveNeeded || this.state.isLoading || this.state.isSaving}
|
||||
onClick={this.handleSubmit}
|
||||
/>
|
||||
{this.state.isSaveNeeded && (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
onClick={this.handleCancel}
|
||||
disabled={this.state.isSaving}
|
||||
style={{marginLeft: '12px'}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className='error-message'>
|
||||
<FormError error={this.state.error}/>
|
||||
</div>
|
||||
|
|
@ -644,7 +1041,10 @@ export function getUserAuthenticationTextField(intl: IntlShape, mfaEnabled: Prop
|
|||
|
||||
if (mfaEnabled) {
|
||||
if (user.mfa_active) {
|
||||
authenticationTextField += ', ';
|
||||
authenticationTextField += intl.formatMessage({
|
||||
id: 'admin.userManagement.userDetail.separator',
|
||||
defaultMessage: ', ',
|
||||
});
|
||||
authenticationTextField += intl.formatMessage({id: 'admin.userManagement.userDetail.mfa', defaultMessage: 'MFA'});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3112,10 +3112,12 @@
|
|||
"admin.user_grid.shared_member": "Shared Member",
|
||||
"admin.user_grid.system_admin": "System Admin",
|
||||
"admin.user_grid.team_admin": "Team Admin",
|
||||
"admin.user_item.cancel": "Cancel",
|
||||
"admin.user_item.deactivate": "Deactivate",
|
||||
"admin.user_item.email_title": "<strong>Email:</strong> {email}",
|
||||
"admin.user_item.guest": "Guest",
|
||||
"admin.user_item.invalidEmail": "Invalid email address",
|
||||
"admin.user_item.invalidUrl": "Invalid URL",
|
||||
"admin.user_item.makeActive": "Activate",
|
||||
"admin.user_item.makeMember": "Make Team Member",
|
||||
"admin.user_item.makeTeamAdmin": "Make Team Admin",
|
||||
|
|
@ -3132,6 +3134,7 @@
|
|||
"admin.user_item.sysAdmin": "System Admin",
|
||||
"admin.user_item.teamAdmin": "Team Admin",
|
||||
"admin.user_item.teamMember": "Team Member",
|
||||
"admin.user_item.unknownError": "Unknown error",
|
||||
"admin.user_item.userActivateFailed": "Failed to activate user",
|
||||
"admin.user_item.userDeactivateFailed": "Failed to deactivate user",
|
||||
"admin.user_item.userMFARemoveFailed": "Failed to remove user's MFA",
|
||||
|
|
@ -3139,12 +3142,21 @@
|
|||
"admin.user_item.userUpdateFailed": "Failed to update user",
|
||||
"admin.user_settings.policy_list.no_policies_found": "No policies found",
|
||||
"admin.user_settings.policy_list.search_policy_errored": "Something went wrong. Try again",
|
||||
"admin.user.selectOptions": "Select options...",
|
||||
"admin.userManagement.userDetail.addTeam": "Add Team",
|
||||
"admin.userManagement.userDetail.arrayValueSeparator": ", ",
|
||||
"admin.userManagement.userDetail.authData": "Auth Data",
|
||||
"admin.userManagement.userDetail.authenticationMethod": "Authentication Method",
|
||||
"admin.userManagement.userDetail.cpaField": "{fieldName}",
|
||||
"admin.userManagement.userDetail.email": "Email",
|
||||
"admin.userManagement.userDetail.ldap": "AD/LDAP: {propertyName}",
|
||||
"admin.userManagement.userDetail.mfa": "MFA",
|
||||
"admin.userManagement.userDetail.noOptions": "No options available",
|
||||
"admin.userManagement.userDetail.notFound": "User not found",
|
||||
"admin.userManagement.userDetail.saml": "SAML: {propertyName}",
|
||||
"admin.userManagement.userDetail.selectOption": "Select an option",
|
||||
"admin.userManagement.userDetail.separator": ", ",
|
||||
"admin.userManagement.userDetail.syncedWith": "Synced with: {source}",
|
||||
"admin.userManagement.userDetail.teamsSubtitle": "Teams to which this user belongs",
|
||||
"admin.userManagement.userDetail.teamsTitle": "Team Membership",
|
||||
"admin.userManagement.userDetail.userId": "User ID: {userId}",
|
||||
|
|
|
|||
|
|
@ -1716,7 +1716,7 @@ describe('Actions.Users', () => {
|
|||
const state = store.getState();
|
||||
const currentUser = state.entities.users.profiles[state.entities.users.currentUserId];
|
||||
|
||||
nock(Client4.getCustomProfileAttributeValuesRoute()).
|
||||
nock(Client4.getUserRoute(currentUser.id) + '/custom_profile_attributes').
|
||||
patch('').
|
||||
query(true).
|
||||
reply(200, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import type {AnyAction} from 'redux';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
|
|
@ -1005,11 +1007,23 @@ export function saveCustomProfileAttribute(userID: string, attributeID: string,
|
|||
return async (dispatch) => {
|
||||
try {
|
||||
const values = {[attributeID]: attributeValue || ''};
|
||||
const data = await Client4.updateCustomProfileAttributeValues(values);
|
||||
|
||||
const data = await Client4.updateUserCustomProfileAttributesValues(userID, values);
|
||||
return {data};
|
||||
} catch (error) {
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
// Extract user-friendly error message from server response
|
||||
let errorMessage = 'Failed to update custom profile attribute';
|
||||
if (error && typeof error === 'object' && 'message' in error && error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
const serverError = {
|
||||
...error,
|
||||
message: errorMessage,
|
||||
};
|
||||
|
||||
dispatch(logError(serverError));
|
||||
return {error: serverError};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import shallowEqual from 'shallow-equals';
|
||||
|
||||
import type {FieldValueType} from '@mattermost/types/properties';
|
||||
|
||||
import {createSelectorCreator, defaultMemoize} from 'mattermost-redux/selectors/create_selector';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
|
|
@ -109,3 +111,8 @@ export function isEmail(email: string): boolean {
|
|||
// this prevents <Outlook Style> outlook.style@domain.com addresses and multiple comma-separated addresses from being accepted
|
||||
return (/^[^ ,@]+@[^ ,@]+$/).test(email);
|
||||
}
|
||||
|
||||
// maps Custom Profile Attribute value types to appropriate HTML schemes (only different for phone -> tel)
|
||||
export function getInputTypeFromValueType(valueType?: FieldValueType): string {
|
||||
return valueType === 'phone' ? 'tel' : String(valueType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2172,6 +2172,13 @@ export default class Client4 {
|
|||
);
|
||||
};
|
||||
|
||||
updateUserCustomProfileAttributesValues = (userID: string, attributeValues: Record<string, string | string[]>) => {
|
||||
return this.doFetch<Record<string, string | string[]>>(
|
||||
`${this.getUserRoute(userID)}/custom_profile_attributes`,
|
||||
{method: 'PATCH', body: JSON.stringify(attributeValues)},
|
||||
);
|
||||
};
|
||||
|
||||
getUserCustomProfileAttributesValues = async (userID: string) => {
|
||||
const data = await this.doFetch<Record<string, string>>(
|
||||
`${this.getUserRoute(userID)}/custom_profile_attributes`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue