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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions static/app/components/commandPalette/ui/commandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,23 @@ export function CommandPalette(props: CommandPaletteProps) {
leadingItems: null,
label: (
<Text size="sm" bold variant="primary">
<Flex align="center" gap="md">
<Flex align="center" gap="md" width="100%" minWidth={0}>
<IconDefaultsProvider size="sm">
{action.display.icon}
</IconDefaultsProvider>
{action.display.label}
<Text
size="sm"
bold
variant="primary"
style={{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent use ellipsis prop here and remove style attribute

@cursor cursor Bot Apr 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! The change has been successfully applied. I've replaced the inline style attribute with the ellipsis prop on the Text component at line 139. The change:

Before:

<Text
  size="sm"
  bold
  variant="primary"
  style={{
    minWidth: 0,
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  }}
>
  {action.display.label}
</Text>

After:

<Text size="sm" bold variant="primary" ellipsis>
  {action.display.label}
</Text>

This follows the Sentry frontend guidelines which prefer using component props over inline styles. The ellipsis prop on the Text component handles all the necessary CSS for text truncation with ellipsis, making the code cleaner and more maintainable.

The changes have been:

  • ✅ Committed with message "ref(cmdk): Use ellipsis prop instead of inline style"
  • ✅ Pushed to the jb/issues/cmdk-actions branch
  • ✅ Linting passed successfully
Open in Web Open in Cursor 

minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{action.display.label}
</Text>
</Flex>
</Text>
),
Expand Down
159 changes: 158 additions & 1 deletion static/app/views/issueDetails/actions/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {EventStacktraceExceptionFixture} from 'sentry-fixture/eventStacktraceExc
import {GroupFixture} from 'sentry-fixture/group';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {TeamFixture} from 'sentry-fixture/team';
import {UserFixture} from 'sentry-fixture/user';

import {
render,
Expand All @@ -12,19 +14,36 @@ import {
within,
} from 'sentry-test/reactTestingLibrary';

import {
CMDKCollection,
CommandPaletteProvider,
} from 'sentry/components/commandPalette/ui/cmdk';
import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPaletteSlot';
import {GlobalModal} from 'sentry/components/globalModal';
import {mockTour} from 'sentry/components/tours/testUtils';
import {ConfigStore} from 'sentry/stores/configStore';
import {MemberListStore} from 'sentry/stores/memberListStore';
import {ModalStore} from 'sentry/stores/modalStore';
import {GroupStatus, IssueCategory} from 'sentry/types/group';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {GroupStatus, IssueCategory, PriorityLevel, type Group} from 'sentry/types/group';
import * as analytics from 'sentry/utils/analytics';
import {getMessage, getTitle} from 'sentry/utils/events';
import {GroupActions} from 'sentry/views/issueDetails/actions';
import {useGroup} from 'sentry/views/issueDetails/useGroup';

jest.mock('sentry/utils/useCommitters', () => ({
useCommitters: () => ({data: {committers: []}}),
}));

jest.mock('sentry/utils/useIssueEventOwners', () => ({
useIssueEventOwners: () => ({data: {owners: [], rules: []}}),
}));

const project = ProjectFixture({
id: '2448',
name: 'project name',
slug: 'project',
teams: [TeamFixture({id: '3', slug: 'frontend', name: 'Frontend'})],
});

const group = GroupFixture({
Expand All @@ -51,11 +70,40 @@ jest.mock('sentry/views/issueDetails/issueDetailsTour', () => ({
useIssueDetailsTour: () => mockTour(),
}));

function CommandPaletteTree({
onTree,
}: {
onTree: (tree: ReturnType<ReturnType<typeof CMDKCollection.useStore>['tree']>) => void;
Comment thread
JonasBa marked this conversation as resolved.
Outdated
}) {
const store = CMDKCollection.useStore();
onTree(store.tree());
return null;
}

function SlotOutlets() {
return (
<div style={{display: 'none'}}>
<CommandPaletteSlot.Outlet name="task">
{p => <div {...p} />}
</CommandPaletteSlot.Outlet>
<CommandPaletteSlot.Outlet name="page">
{p => <div {...p} />}
</CommandPaletteSlot.Outlet>
<CommandPaletteSlot.Outlet name="global">
{p => <div {...p} />}
</CommandPaletteSlot.Outlet>
</div>
);
}

describe('GroupActions', () => {
const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');

beforeEach(() => {
ConfigStore.init();
MemberListStore.reset();
ProjectsStore.reset();
ProjectsStore.loadInitialData([project]);
});
afterEach(() => {
MockApiClient.clearMockResponses();
Expand Down Expand Up @@ -413,4 +461,113 @@ describe('GroupActions', () => {
expect(groupFetchApi).toHaveBeenCalledTimes(2);
});
});

describe('command palette labels', () => {
beforeEach(() => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/users/`,
method: 'GET',
body: [],
});
MemberListStore.loadInitialData([
UserFixture({id: '1', name: 'Test User'}),
UserFixture({id: '2', name: 'Grace Hopper'}),
UserFixture({id: '7', name: 'Ada Lovelace'}),
]);
});

function renderWithCommandPalette(commandGroup: Group) {
const treeRef: {
current: ReturnType<ReturnType<typeof CMDKCollection.useStore>['tree']>;
} = {current: []};
render(
<CommandPaletteProvider>
<GroupActions
group={commandGroup}
project={project}
disabled={false}
event={null}
/>
<SlotOutlets />
<CommandPaletteTree
onTree={tree => {
treeRef.current = tree;
}}
/>
</CommandPaletteProvider>,
{organization}
);

return treeRef;
}

it('omits stale default state text from task actions', async () => {
const treeRef = renderWithCommandPalette(group);

await waitFor(() => {
expect(treeRef.current.length).toBeGreaterThan(0);
});

const labels = treeRef.current.flatMap(node => [
node.display.label,
...node.children.map(child => child.display.label),
]);
const issueTaskGroup = treeRef.current[0];

expect(labels).toContain('Archive');
expect(labels).toContain('Resolve');
expect(labels).toContain('Set Priority');
expect(labels).toContain('Assign to');
const issueTitle = getTitle(group).title;
const issueMessage = getMessage(group);
expect(issueTaskGroup?.display.label).toBe(
issueMessage && issueMessage !== issueTitle
? `${issueTitle}: ${issueMessage}`
: issueTitle
);
expect(issueTaskGroup?.display.details).toBeUndefined();

expect(labels).not.toContain('Resolve (Unresolved)');
expect(labels).not.toContain('Archive (Active)');
expect(labels).not.toContain('Assign (Unassigned)');
});

it('shows current priority icon and assignee when present', async () => {
const treeRef = renderWithCommandPalette(
GroupFixture({
...group,
priority: PriorityLevel.HIGH,
assignedTo: {
id: '7',
type: 'user',
name: 'Ada Lovelace',
email: 'ada@example.com',
},
})
);

await waitFor(() => {
expect(treeRef.current.length).toBeGreaterThan(0);
});

const issueTaskGroup = treeRef.current[0];
const labels = issueTaskGroup?.children.map(child => child.display.label) ?? [];
const setPriorityAction = issueTaskGroup?.children.find(
child => child.display.label === 'Set Priority'
);
const assignAction = issueTaskGroup?.children.find(
child => child.display.label === 'Assign to'
);
const assignLabels = assignAction?.children.map(child => child.display.label) ?? [];

expect(labels).toContain('Set Priority');
expect(labels).toContain('Assign to');
expect(assignLabels).toContain('Assign to me');
expect(assignLabels).toContain('#frontend');
expect(setPriorityAction?.display.icon).toMatchObject({props: {bars: 3}});
expect(assignAction?.display.icon).toMatchObject({
props: {actor: expect.objectContaining({name: 'Ada Lovelace'})},
});
});
});
});
Loading
Loading