Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions workspaces/lightspeed/.changeset/hot-bottles-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': minor
---

feat: add conversation sorting with persistence, persisting pinned chats and pinned chats toggle per-user
58 changes: 53 additions & 5 deletions workspaces/lightspeed/packages/app/e2e-tests/lightspeed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ import {
verifyNoResultsFoundMessage,
verifyChatUnpinned,
clearSearch,
openSortDropdown,
verifySortDropdownOptions,
selectSortOption,
verifySortDropdownVisible,
closeSortDropdown,
verifyConversationsSortedAlphabetically,
} from './utils/chatManagement';
import { login } from './utils/login';
import {
Expand Down Expand Up @@ -410,6 +416,7 @@ test.describe('Lightspeed tests', () => {
: test.describe.skip;
chatManagementDescribeFn('Chat Management', () => {
const testChatName = 'Test Rename';
const deleteChatName = 'Conversation 1';

test('Verify chat actions menu', async () => {
await sharedPage.reload();
Expand All @@ -431,20 +438,30 @@ test.describe('Lightspeed tests', () => {
await selectPinAction(sharedPage, translations);
await verifyChatPinned(sharedPage, testChatName);
await verifyPinnedChatsNotEmpty(sharedPage, translations);
await sharedPage.reload();
await verifyPinnedChatsNotEmpty(sharedPage, translations);
});

test('Verify delete chat and its actions', async () => {
await verifyChatRenamed(sharedPage, testChatName);
await openChatContextMenuByName(sharedPage, testChatName, translations);
await verifyChatRenamed(sharedPage, deleteChatName);
await openChatContextMenuByName(
sharedPage,
deleteChatName,
translations,
);
await selectDeleteAction(sharedPage, translations);
await verifyDeleteConfirmation(sharedPage, translations);
await cancelChatDeletion(sharedPage, translations);
await verifyChatRenamed(sharedPage, testChatName);
await verifyChatRenamed(sharedPage, deleteChatName);

await openChatContextMenuByName(sharedPage, testChatName, translations);
await openChatContextMenuByName(
sharedPage,
deleteChatName,
translations,
);
await selectDeleteAction(sharedPage, translations);
await confirmChatDeletion(sharedPage, translations);
await verifyChatDeleted(sharedPage, testChatName);
await verifyChatDeleted(sharedPage, deleteChatName);
});

test('Verify disable pinned chats section via settings', async () => {
Expand Down Expand Up @@ -490,6 +507,37 @@ test.describe('Lightspeed tests', () => {
await selectUnpinAction(sharedPage, translations);
await verifyChatUnpinned(sharedPage, translations);
});

test('Verify sort dropdown is available', async () => {
await verifySortDropdownVisible(sharedPage, translations);
await openSortDropdown(sharedPage, translations);
await verifySortDropdownOptions(sharedPage, translations);
await closeSortDropdown(sharedPage);
});

test('Verify conversations are sorted correctly', async () => {
// Verify alphabetical ascending sort (A-Z)
await openSortDropdown(sharedPage, translations);
await selectSortOption(sharedPage, 'alphabeticalAsc', translations);
await verifyConversationsSortedAlphabetically(
sharedPage,
translations,
'asc',
);

// Verify alphabetical descending sort (Z-A)
await openSortDropdown(sharedPage, translations);
await selectSortOption(sharedPage, 'alphabeticalDesc', translations);
await verifyConversationsSortedAlphabetically(
sharedPage,
translations,
'desc',
);

// Reset to newest first
await openSortDropdown(sharedPage, translations);
await selectSortOption(sharedPage, 'newest', translations);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,96 @@ export const verifyChatUnpinned = async (
.filter({ hasText: translations['chatbox.emptyState.noPinnedChats'] }),
).toBeVisible();
};

export type SortOption =
| 'newest'
| 'oldest'
| 'alphabeticalAsc'
| 'alphabeticalDesc';

export const openSortDropdown = async (
page: Page,
translations: LightspeedMessages,
) => {
await page.getByRole('button', { name: translations['sort.label'] }).click();
};

export const verifySortDropdownOptions = async (
page: Page,
translations: LightspeedMessages,
) => {
await expect(page.locator('#sort-select')).toMatchAriaSnapshot(`
- listbox:
- option "${translations['sort.newest']}"
- option "${translations['sort.oldest']}"
- option "${translations['sort.alphabeticalAsc']}"
- option "${translations['sort.alphabeticalDesc']}"
`);
};

export const selectSortOption = async (
page: Page,
sortOption: SortOption,
translations: LightspeedMessages,
) => {
const sortOptionLabels: Record<SortOption, keyof LightspeedMessages> = {
newest: 'sort.newest',
oldest: 'sort.oldest',
alphabeticalAsc: 'sort.alphabeticalAsc',
alphabeticalDesc: 'sort.alphabeticalDesc',
};
await page
.getByRole('option', { name: translations[sortOptionLabels[sortOption]] })
.click();
};

export const getConversationNames = async (
page: Page,
translations: LightspeedMessages,
): Promise<string[]> => {
const sidePanel = page.locator('.pf-v6-c-drawer__panel-main');
const recentSection = sidePanel.locator(
`ul[aria-label="${translations['conversation.category.recent']}"] li.pf-chatbot__menu-item`,
);

const chatItems = await recentSection.all();
const names: string[] = [];

for (const item of chatItems) {
const text = await item.textContent();
if (text) {
names.push(text.trim());
}
}

return names;
};

export const verifyConversationsSortedAlphabetically = async (
page: Page,
translations: LightspeedMessages,
order: 'asc' | 'desc' = 'asc',
) => {
const conversationNames = await getConversationNames(page, translations);

const sortedNames = [...conversationNames].sort((a, b) =>
order === 'asc'
? a.localeCompare(b, undefined, { sensitivity: 'base' })
: b.localeCompare(a, undefined, { sensitivity: 'base' }),
);

expect(conversationNames).toEqual(sortedNames);
};

export const verifySortDropdownVisible = async (
page: Page,
translations: LightspeedMessages,
) => {
await expect(
page.getByRole('button', { name: translations['sort.label'] }),
).toBeVisible();
};

export const closeSortDropdown = async (page: Page) => {
await page.keyboard.press('Escape');
};
4 changes: 3 additions & 1 deletion workspaces/lightspeed/plugins/lightspeed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@
"@backstage/theme": "^0.7.0",
"@material-ui/core": "^4.9.13",
"@material-ui/lab": "^4.0.0-alpha.61",
"@monaco-editor/react": "^4.7.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this PR adding monaco editor as a dependency here?

Copy link
Member Author

Choose a reason for hiding this comment

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

@monaco-editor/react and @monaco-editor are required peer dependencies of @patternfly/[email protected]. We upgraded to this prerelease version to use the new searchActionEnd prop on ChatbotConversationHistoryNav for the sort dropdown feature.

Copy link
Member

@debsmita1 debsmita1 Jan 9, 2026

Choose a reason for hiding this comment

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

The chatbot pre-release verison adds monaco-editor as a peer-dependency, without this in our dependency shows module not found errs

"@mui/icons-material": "^6.1.8",
"@mui/material": "^5.12.2",
"@patternfly/chatbot": "6.4.1",
"@patternfly/chatbot": "6.5.0-prerelease.28",
"@patternfly/react-core": "6.4.0",
"@patternfly/react-icons": "^6.3.1",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common": "workspace:^",
"@tanstack/react-query": "^5.59.15",
"monaco-editor": "^0.54.0",
"react-markdown": "^9.0.1",
"react-use": "^17.2.4"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ readonly "settings.pinned.enable": string;
readonly "settings.pinned.disable": string;
readonly "settings.pinned.enabled.description": string;
readonly "settings.pinned.disabled.description": string;
readonly "sort.label": string;
readonly "sort.newest": string;
readonly "sort.oldest": string;
readonly "sort.alphabeticalAsc": string;
readonly "sort.alphabeticalDesc": string;
}>;

// @alpha
Expand Down
Loading