mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: offer the tags of the selection's account in the context menu, like the toolbar
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({
|
||||
PluginSlot: () => null,
|
||||
}));
|
||||
|
||||
import { EmailContextMenu } from '../email-context-menu';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { loadSharedTags, resetSharedTags, sharedTagsKey, useSharedTagsStore } from '@/stores/shared-tags-store';
|
||||
|
||||
// The context menu's tag list for a selection offers the tags of the
|
||||
// selection's account (commonTagScope, like the toolbar), not those of the
|
||||
// row that was right-clicked.
|
||||
|
||||
const TEAM = { loginId: 'login-1', accountId: 'team' };
|
||||
|
||||
// A message in the group account, right-clicked.
|
||||
const email = {
|
||||
id: 'e1',
|
||||
threadId: 't1',
|
||||
mailboxIds: { 'team:inbox': true },
|
||||
keywords: { $seen: true },
|
||||
from: [{ email: 'alice@example.com' }],
|
||||
subject: 'Hello',
|
||||
receivedAt: '2026-01-01T00:00:00Z',
|
||||
size: 100,
|
||||
sourceClientAccountId: 'login-1',
|
||||
sourceAccountId: 'team',
|
||||
} as unknown as Email;
|
||||
|
||||
const mailboxes = [{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } }] as unknown as Mailbox[];
|
||||
|
||||
function openTags(batchTagScope: typeof TEAM | null) {
|
||||
render(
|
||||
<EmailContextMenu
|
||||
email={email}
|
||||
position={{ x: 10, y: 10 }}
|
||||
isOpen
|
||||
onClose={vi.fn()}
|
||||
menuRef={{ current: null }}
|
||||
mailboxes={mailboxes}
|
||||
selectedMailbox="inbox"
|
||||
currentMailboxRole="inbox"
|
||||
isMultiSelect
|
||||
selectedCount={2}
|
||||
batchTagIds={[]}
|
||||
batchPartialTagIds={[]}
|
||||
batchTagScope={batchTagScope}
|
||||
onBatchToggleTag={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
act(() => { fireEvent.mouseEnter(screen.getByTestId('ctx-tag')); });
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
resetSharedTags();
|
||||
// The group's tags come through the real loading path.
|
||||
const client = {
|
||||
getAccountId: () => 'me',
|
||||
getSieveAccountId: () => 'me',
|
||||
supportsSieve: () => false,
|
||||
getSharedAccounts: () => [],
|
||||
getAccountInfo: () => ({ isPersonal: false }),
|
||||
getPrincipalTypes: async () => ({ team: 'group' }),
|
||||
advertisesBulwarkMetadata: () => true,
|
||||
supportsBulwarkMetadata: () => true,
|
||||
getBulwarkMetadata: async () => ({ tags: { tags: { urgent: { label: 'Team urgent', color: 'red' } } } }),
|
||||
discoverKeywords: async () => ({ keywords: {}, scanned: 0, total: 0, complete: true }),
|
||||
};
|
||||
useAuthStore.setState({
|
||||
client: client as never,
|
||||
activeAccountId: 'login-1',
|
||||
getClientForAccount: ((id: string) => (id === 'login-1' ? client : undefined)) as never,
|
||||
});
|
||||
useSettingsStore.setState({ emailKeywords: [{ id: 'mine', label: 'Mine', color: 'blue' }], nestedTags: false });
|
||||
await loadSharedTags(sharedTagsKey(TEAM.loginId, TEAM.accountId), TEAM.accountId, client);
|
||||
expect(useSharedTagsStore.getState().entries[sharedTagsKey(TEAM.loginId, TEAM.accountId)]?.status).toBe('ready');
|
||||
});
|
||||
afterEach(() => resetSharedTags());
|
||||
|
||||
describe('EmailContextMenu tags for a selection', () => {
|
||||
it('offers the group\'s shared tags when the whole selection is in the group', async () => {
|
||||
openTags(TEAM);
|
||||
expect(await screen.findByText('Team urgent')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Mine')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers the own tags when the selection mixes accounts, even from a group row', async () => {
|
||||
openTags(null);
|
||||
expect(await screen.findByText('Mine')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Team urgent')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useEmailStore, removalFolder, canRemoveFromFolder, emailLinkSource } from "@/stores/email-store";
|
||||
import { useEmailTagScope } from "@/hooks/use-account-tags";
|
||||
import { useEmailTagScope, type TagScope } from "@/hooks/use-account-tags";
|
||||
import { Email, Mailbox } from "@/lib/jmap/types";
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -113,6 +113,11 @@ interface EmailContextMenuProps {
|
||||
/** Tags every selected message carries, and those only some of them do. */
|
||||
batchTagIds?: string[];
|
||||
batchPartialTagIds?: string[];
|
||||
/**
|
||||
* The account of the whole selection (commonTagScope): its tags are
|
||||
* offered. Null when the selection mixes accounts: the user's own tags.
|
||||
*/
|
||||
batchTagScope?: TagScope | null;
|
||||
/** Takes the tag off when the whole selection has it, else puts it on all. */
|
||||
onBatchToggleTag?: (tagId: string) => void;
|
||||
onBatchDelete?: () => void;
|
||||
@@ -180,6 +185,7 @@ export function EmailContextMenu({
|
||||
onBatchMarkAsRead,
|
||||
batchTagIds,
|
||||
batchPartialTagIds,
|
||||
batchTagScope = null,
|
||||
onBatchToggleTag,
|
||||
onBatchDelete,
|
||||
onBatchArchive,
|
||||
@@ -207,7 +213,7 @@ export function EmailContextMenu({
|
||||
const isPinned = email.keywords?.['$pinned'] === true;
|
||||
const isDraft = email.keywords?.['$draft'] === true;
|
||||
const currentTagIds = getEmailTagIds(email.keywords);
|
||||
// The row's account names the tags, for a selection too: it is part of it.
|
||||
// The row's account names its tags; a selection uses batchTagScope.
|
||||
const tagScope = useEmailTagScope(email);
|
||||
const showBatchActions = isMultiSelect && selectedCount > 1;
|
||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||
@@ -500,7 +506,7 @@ export function EmailContextMenu({
|
||||
selectedIds={batchTagIds ?? []}
|
||||
partialIds={batchPartialTagIds}
|
||||
onToggle={(tagId) => onBatchToggleTag?.(tagId)}
|
||||
scope={tagScope}
|
||||
scope={batchTagScope}
|
||||
/>
|
||||
) : (
|
||||
<TagPicker
|
||||
|
||||
@@ -1173,6 +1173,7 @@ export function EmailList({
|
||||
onBatchMarkAsRead={(read) => markSelectionRead(read)}
|
||||
batchTagIds={selectionTags.all}
|
||||
batchPartialTagIds={selectionTags.some}
|
||||
batchTagScope={selectionTags.scope}
|
||||
onBatchToggleTag={handleBatchToggleTag}
|
||||
onBatchDelete={async () => {
|
||||
if (!client) return;
|
||||
|
||||
Reference in New Issue
Block a user