mirror of
https://github.com/mattermost/mattermost.git
synced 2026-11-03 12:48:37 -06:00
[MM-70833] Stop dimming the Classification row in Attribute Management (#38693)
This commit is contained in:
1 parent
f0b63f8ed1
commit
50a91ddb2a
4 files changed
+50
-91
No files matched your search
+19
-22
@@ -10,7 +10,7 @@
|
||||
* Professional-only licenses hide this admin route (React Router redirects away).
|
||||
*/
|
||||
|
||||
import {expect, test, getAdminClient, licenseTier} from '@mattermost/playwright-lib';
|
||||
import {expect, test, getAdminClient} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
CLASSIFICATION_MARKINGS_ADMIN_PATH,
|
||||
@@ -303,8 +303,9 @@ test.describe('System Console - Global Attributes listing', {tag: '@system_conso
|
||||
/**
|
||||
* @objective Ensure a real Classification Markings field (name/object_type/group_id
|
||||
* matching production's saveCreateField) renders the read-only subtitle and an
|
||||
* open-in-new link to its own admin page instead of the ordinary dot-menu, and that an
|
||||
* unrelated field — including one that shares the same 'rank' type — is entirely unaffected.
|
||||
* open-in-new link to its own admin page instead of the ordinary dot-menu, that its name
|
||||
* still renders at the same contrast as any other row's, and that an unrelated field —
|
||||
* including one that shares the same 'rank' type — is entirely unaffected.
|
||||
*/
|
||||
test(
|
||||
'renders the Classification Markings row as a read-only open-in-new link, leaving an unrelated rank field unaffected',
|
||||
@@ -312,17 +313,6 @@ test.describe('System Console - Global Attributes listing', {tag: '@system_conso
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await requireGlobalAttributesEnabled(pw);
|
||||
|
||||
// Mirrors useClassificationAttributePageReachable (global_attributes_table.tsx):
|
||||
// Enterprise Advanced tier + the ChannelAttributes flag together make the
|
||||
// classification attribute page reachable, which adds a dot-menu (Edit there)
|
||||
// alongside the row's always-present open-in-new link.
|
||||
const [license, config] = await Promise.all([
|
||||
adminClient.getClientLicenseOld(),
|
||||
adminClient.getConfig(),
|
||||
]);
|
||||
const classificationAttributePageReachable =
|
||||
licenseTier(license.SkuShortName) >= 30 && config.FeatureFlags.ChannelAttributes === true;
|
||||
|
||||
// # The link's destination page is gated by its own independent feature flag
|
||||
// (ClassificationMarkings) — it must be on for the link to render.
|
||||
// Tagged @classification_markings like every other spec that touches this same
|
||||
@@ -391,17 +381,12 @@ test.describe('System Console - Global Attributes listing', {tag: '@system_conso
|
||||
'Classification Markings',
|
||||
);
|
||||
|
||||
// * The rightmost cell always carries an open-in-new link to the
|
||||
// Classification Markings admin page
|
||||
// * The rightmost cell carries only an open-in-new link to the
|
||||
// Classification Markings admin page — no Edit/More-actions menu
|
||||
const openInNewLink = classificationRow.getByRole('link', {name: 'Open Classification Markings'});
|
||||
await expect(openInNewLink).toBeVisible();
|
||||
await expect(openInNewLink).toHaveAttribute('href', CLASSIFICATION_MARKINGS_ADMIN_PATH);
|
||||
|
||||
// * A dot-menu (offering Edit to the classification attribute page) appears
|
||||
// alongside the open-in-new link only when that page is actually reachable.
|
||||
await expect(classificationRow.getByRole('button', {name: 'More actions'})).toHaveCount(
|
||||
classificationAttributePageReachable ? 1 : 0,
|
||||
);
|
||||
await expect(classificationRow.getByRole('button', {name: 'More actions'})).toHaveCount(0);
|
||||
|
||||
// # Clicking the link actually navigates to the Classification Markings page
|
||||
await openInNewLink.click();
|
||||
@@ -421,6 +406,18 @@ test.describe('System Console - Global Attributes listing', {tag: '@system_conso
|
||||
await expect(unrelatedRow.getByText('Read-only')).toHaveCount(0);
|
||||
await expect(unrelatedRow.getByRole('link', {name: 'Open Classification Markings'})).toHaveCount(0);
|
||||
await expect(unrelatedRow.getByTestId('global-attribute-source')).toContainText('Managed here');
|
||||
|
||||
// * Classification's name renders at the same contrast as any other row's:
|
||||
// a definition that lives on another page must not read as a disabled
|
||||
// attribute. Compared against the sibling row rather than a literal colour
|
||||
// so it holds under every theme.
|
||||
const unrelatedNameColor = await unrelatedRow
|
||||
.getByTestId('global-attribute-name')
|
||||
.evaluate((el) => getComputedStyle(el).color);
|
||||
await expect(classificationRow.getByTestId('global-attribute-name')).toHaveCSS(
|
||||
'color',
|
||||
unrelatedNameColor,
|
||||
);
|
||||
} finally {
|
||||
await deleteClassificationMarkingsFieldIfExists(adminClient);
|
||||
await deleteGlobalAttributeFieldIfExists(adminClient, unrelatedRankName);
|
||||
|
||||
-14
@@ -65,26 +65,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.GlobalAttributesTable__name--classification {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
.GlobalAttributesTable__subtitle {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.GlobalAttributesTable__subtitle--classification {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
.GlobalAttributesTable__actions--classification {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.GlobalAttributesTable__link--classification {
|
||||
display: inline-flex;
|
||||
width: 32px;
|
||||
|
||||
+22
-4
@@ -1452,7 +1452,7 @@ describe('GlobalAttributesTable', () => {
|
||||
});
|
||||
|
||||
describe('Classification Markings row', () => {
|
||||
it('renders the subtitle, an open-in-new link and a menu carrying Edit when the field matches and the destination is reachable', async () => {
|
||||
it('renders the subtitle and an open-in-new link with no actions menu when the field matches and the destination is reachable', async () => {
|
||||
getPropertyFields.mockResolvedValueOnce([makeClassificationField()]).mockResolvedValue([]);
|
||||
|
||||
renderWithContext(<GlobalAttributesTable/>, getReachableState());
|
||||
@@ -1464,9 +1464,9 @@ describe('GlobalAttributesTable', () => {
|
||||
expect(link).toHaveAttribute('href', CLASSIFICATIONS_MARKINGS_ADMIN_URL);
|
||||
expect(link).toHaveAccessibleName('Open Classification Markings');
|
||||
|
||||
// * Unlike every other row, this one's Edit is live: it is the only way to
|
||||
// configure classification's resources outside the API
|
||||
expect(screen.getByTestId('global-attribute-actions-field-1')).toBeInTheDocument();
|
||||
// * The definition is edited on Classification Markings; this row must not
|
||||
// offer a duplicate Edit/More-actions menu next to the open-in-new link
|
||||
expect(screen.queryByTestId('global-attribute-actions-field-1')).not.toBeInTheDocument();
|
||||
|
||||
// * The Source column also identifies this row's true source, rather than the
|
||||
// generic "Managed here" every other native field gets
|
||||
@@ -1534,6 +1534,24 @@ describe('GlobalAttributesTable', () => {
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not dim the name or subtitle, so a read-only definition does not read as a disabled attribute', async () => {
|
||||
getPropertyFields.
|
||||
mockResolvedValueOnce([makeClassificationField(), makeField({id: 'field-2', attrs: {display_name: 'Program'}})]).
|
||||
mockResolvedValue([]);
|
||||
|
||||
renderWithContext(<GlobalAttributesTable/>, getReachableState());
|
||||
|
||||
const [classificationName, ordinaryName] = await screen.findAllByTestId('global-attribute-name');
|
||||
expect(classificationName).toHaveTextContent('Classification');
|
||||
expect(ordinaryName).toHaveTextContent('Program');
|
||||
|
||||
// * Dimming the row on top of its "Definition is read-only" subtitle made a
|
||||
// correctly configured attribute look disabled
|
||||
expect(classificationName.className).toBe(ordinaryName.className);
|
||||
expect(screen.getByTestId('global-attribute-classification-subtitle-field-1').className).
|
||||
toBe('GlobalAttributesTable__subtitle');
|
||||
});
|
||||
|
||||
it('leaves an unrelated field (not matching name/object_type/group_id) entirely unaffected even when the destination is reachable', async () => {
|
||||
getPropertyFields.mockResolvedValueOnce([makeField({type: 'rank'})]).mockResolvedValue([]);
|
||||
|
||||
|
||||
+9
-51
@@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createColumnHelper, getCoreRowModel, useReactTable, type ColumnDef} from '@tanstack/react-table';
|
||||
import classNames from 'classnames';
|
||||
import type {ComponentType} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
@@ -239,7 +238,7 @@ function AttributeCell({field, isClassificationRow}: ClassificationAwareCellProp
|
||||
return (
|
||||
<span className='GlobalAttributesTable__attribute'>
|
||||
<span
|
||||
className={classNames('GlobalAttributesTable__name', {'GlobalAttributesTable__name--classification': isClassificationRow})}
|
||||
className='GlobalAttributesTable__name'
|
||||
data-testid='global-attribute-name'
|
||||
>
|
||||
{getDisplayName(field)}
|
||||
@@ -247,7 +246,7 @@ function AttributeCell({field, isClassificationRow}: ClassificationAwareCellProp
|
||||
{isClassificationRow && (
|
||||
<span
|
||||
id={classificationSubtitleId(field.id)}
|
||||
className='GlobalAttributesTable__subtitle GlobalAttributesTable__subtitle--classification'
|
||||
className='GlobalAttributesTable__subtitle'
|
||||
data-testid={`global-attribute-classification-subtitle-${field.id}`}
|
||||
>
|
||||
<FormattedMessage {...messages.classificationSubtitle}/>
|
||||
@@ -258,14 +257,13 @@ function AttributeCell({field, isClassificationRow}: ClassificationAwareCellProp
|
||||
}
|
||||
|
||||
type ActionsCellProps = ClassificationAwareCellProps & {
|
||||
canEditClassification: boolean;
|
||||
isMobileView: boolean;
|
||||
pluginInventoryLoaded: boolean;
|
||||
onDeleteError: (message: string | null) => void;
|
||||
onDeleteModalExited: () => void;
|
||||
};
|
||||
|
||||
function ActionsCell({field, isClassificationRow, canEditClassification, isMobileView, pluginInventoryLoaded, onDeleteError, onDeleteModalExited}: ActionsCellProps) {
|
||||
function ActionsCell({field, isClassificationRow, isMobileView, pluginInventoryLoaded, onDeleteError, onDeleteModalExited}: ActionsCellProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const promptDelete = useGlobalAttributeFieldDelete();
|
||||
@@ -295,9 +293,12 @@ function ActionsCell({field, isClassificationRow, canEditClassification, isMobil
|
||||
}
|
||||
}, [dispatch, field.id, formatMessage, onDeleteError]);
|
||||
|
||||
// Classification's definition lives on Classification Markings; this row only
|
||||
// offers the open-in-new link there. Row click still opens the attribute page
|
||||
// when that route is reachable.
|
||||
if (isClassificationRow) {
|
||||
const classificationLinkLabel = formatMessage(actionsLabels.classificationLink);
|
||||
const externalLink = (
|
||||
return (
|
||||
<WithTooltip
|
||||
title={classificationLinkLabel}
|
||||
disabled={isMobileView}
|
||||
@@ -316,47 +317,6 @@ function ActionsCell({field, isClassificationRow, canEditClassification, isMobil
|
||||
</Link>
|
||||
</WithTooltip>
|
||||
);
|
||||
|
||||
if (!canEditClassification) {
|
||||
return externalLink;
|
||||
}
|
||||
|
||||
// Both destinations: Edit configures which resources classification applies
|
||||
// to, the link goes to where its levels are defined.
|
||||
return (
|
||||
<div className='GlobalAttributesTable__actions--classification'>
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: `${menuId}-button`,
|
||||
class: 'btn btn-transparent GlobalAttributesTable__actionsButton',
|
||||
children: <DotsHorizontalIcon size={18}/>,
|
||||
dataTestId: menuId,
|
||||
'aria-label': formatMessage(actionsLabels.tooltip),
|
||||
}}
|
||||
menuButtonTooltip={{text: formatMessage(actionsLabels.tooltip)}}
|
||||
menu={{
|
||||
id: `${menuId}-menu`,
|
||||
'aria-label': formatMessage(actionsLabels.menuLabel),
|
||||
}}
|
||||
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
|
||||
transformOrigin={{vertical: 'top', horizontal: 'right'}}
|
||||
>
|
||||
<Menu.LinkItem
|
||||
id={`${menuId}-edit`}
|
||||
to={CLASSIFICATION_ATTRIBUTE_ROUTE}
|
||||
leadingElement={<PencilOutlineIcon size={18}/>}
|
||||
labels={<span><FormattedMessage {...actionsLabels.edit}/></span>}
|
||||
/>
|
||||
<Menu.LinkItem
|
||||
id={`${menuId}-markings`}
|
||||
to={CLASSIFICATIONS_MARKINGS_ADMIN_URL}
|
||||
leadingElement={<OpenInNewIcon size={18}/>}
|
||||
labels={<span><FormattedMessage {...actionsLabels.classificationLink}/></span>}
|
||||
/>
|
||||
</Menu.Container>
|
||||
{externalLink}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -594,8 +554,7 @@ export default function GlobalAttributesTable({searchQuery = ''}: GlobalAttribut
|
||||
return;
|
||||
}
|
||||
|
||||
// Same destinations as the row's Edit/View action: classification has
|
||||
// its own page (or the markings page when that edit route is hidden).
|
||||
// Classification has its own page (or the markings page when that edit route is hidden).
|
||||
if (isClassificationMarkingsField(field, groupId) && classificationMarkingsReachable) {
|
||||
getHistory().push(classificationAttributePageReachable ? CLASSIFICATION_ATTRIBUTE_ROUTE : CLASSIFICATIONS_MARKINGS_ADMIN_URL);
|
||||
return;
|
||||
@@ -688,7 +647,6 @@ export default function GlobalAttributesTable({searchQuery = ''}: GlobalAttribut
|
||||
<ActionsCell
|
||||
field={row.original}
|
||||
isClassificationRow={isClassificationRow(row.original)}
|
||||
canEditClassification={classificationAttributePageReachable}
|
||||
isMobileView={isMobileView}
|
||||
pluginInventoryLoaded={pluginInventoryLoadedRef.current}
|
||||
onDeleteError={setDeleteError}
|
||||
@@ -699,7 +657,7 @@ export default function GlobalAttributesTable({searchQuery = ''}: GlobalAttribut
|
||||
enableHiding: false,
|
||||
}),
|
||||
];
|
||||
}, [appliesToByTemplateId, groupId, classificationMarkingsReachable, classificationAttributePageReachable, isMobileView, handleDeleteModalExited]);
|
||||
}, [appliesToByTemplateId, groupId, classificationMarkingsReachable, isMobileView, handleDeleteModalExited]);
|
||||
|
||||
const table = useReactTable<PropertyField>({
|
||||
data: rows,
|
||||
|
||||
Reference in new issue
Block a user