feat(messaging-ui): import a channel's roster from the settings panel
Phase C of channel-roster import. Adds the optional addMembers(threadId, userIds) adapter seam (+ BulkAddResult) and, in ConversationSettings, a '#' mode on the existing Add-people box that lists the channels you belong to — public AND private, since the source list is your own membership-scoped conversation list. Picking a channel STAGES the import (reads its roster, diffs against who is already here) and shows a confirm — 'Add 9 people from #design? (3 already here)' — so an administrative action never fires on a stray click. The result line reports added / already-a-member / failed. Absent addMembers => the affordance is hidden entirely and '#' is just a search string. Bumped to 0.1.8. SDK suite: 15 files / 78 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,4 +37,61 @@ describe('<ConversationSettings />', () => {
|
||||
expect((await a.listMembers('th_mock_2')).some((m) => m.id === 'pp_sofia')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('“#” switches the picker to channels you belong to, excluding this one', async () => {
|
||||
mount();
|
||||
await screen.findByText('Dan Whitaker');
|
||||
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
|
||||
// Channels the mock user is in show up as import sources…
|
||||
await screen.findByText('#general');
|
||||
// …and the conversation being edited is never offered as its own source.
|
||||
expect(screen.queryByText('#Storm crew')).toBeNull();
|
||||
});
|
||||
|
||||
it('imports a channel roster only after confirmation, and reports what landed', async () => {
|
||||
const a = mount();
|
||||
await screen.findByText('Dan Whitaker');
|
||||
const before = (await a.listMembers('th_mock_2')).length;
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
|
||||
fireEvent.click(await screen.findByText('#general'));
|
||||
|
||||
// Staged, NOT applied — picking a channel must not mutate membership on its own.
|
||||
const confirm = await screen.findByRole('button', { name: /^Add \d+$/ });
|
||||
expect((await a.listMembers('th_mock_2')).length).toBe(before);
|
||||
|
||||
fireEvent.click(confirm);
|
||||
await waitFor(async () => {
|
||||
expect((await a.listMembers('th_mock_2')).length).toBeGreaterThan(before);
|
||||
});
|
||||
await screen.findByText(/Added \d+/);
|
||||
});
|
||||
|
||||
it('cancelling a staged import leaves membership untouched', async () => {
|
||||
const a = mount();
|
||||
await screen.findByText('Dan Whitaker');
|
||||
const before = (await a.listMembers('th_mock_2')).length;
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
|
||||
fireEvent.click(await screen.findByText('#general'));
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
|
||||
|
||||
await screen.findByLabelText('Search people'); // back to the picker
|
||||
expect((await a.listMembers('th_mock_2')).length).toBe(before);
|
||||
});
|
||||
|
||||
it('hides the channel-import affordance when the adapter cannot bulk-add', async () => {
|
||||
const a = new MockAdapter();
|
||||
// A host that implements single add but not addMembers => no import path offered.
|
||||
(a as { addMembers?: unknown }).addMembers = undefined;
|
||||
render(
|
||||
<MessagingProvider adapter={a}>
|
||||
<ConversationSettings threadId="th_mock_2" title="Storm crew" membership="group" onClose={() => {}} />
|
||||
</MessagingProvider>,
|
||||
);
|
||||
await screen.findByText('Dan Whitaker');
|
||||
expect(screen.getByLabelText('Search people').getAttribute('placeholder')).toBe('Search people…');
|
||||
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
|
||||
expect(screen.queryByText('#general')).toBeNull(); // '#' is just a search string here
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user