Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(clerk-js): Use Gate in OrganizationSwitcher by panteliselef · Pull Request #1851 · clerk/javascript · GitHub
Skip to content
Merged
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 .changeset/shy-seahorses-begin.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Replace role based check with permission based checks inside the OrganizationSwitcher component.
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,10 +285,8 @@ describe('OrganizationMembers', () => {
total_count: 2,
}),
);
const { queryByText, getByRole } = render(<OrganizationMembers />, { wrapper });
await waitFor(async () => {
await userEvent.click(getByRole('tab', { name: 'Invitations' }));
});
const { queryByText, findByRole } = render(<OrganizationMembers />, { wrapper });
await userEvent.click(await findByRole('tab', { name: 'Invitations' }));
expect(fixtures.clerk.organization?.getInvitations).toHaveBeenCalled();
expect(queryByText('admin1@clerk.dev')).toBeInTheDocument();
expect(queryByText('Admin')).toBeInTheDocument();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types';
import React from 'react';

import { runIfFunctionOrReturn } from '../../../utils';
import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreClerk,
useCoreOrganization,
Expand DownExpand Up@@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
},
);

const NotificationCountBadgeManageButton = () => {
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const NotificationCountBadgeManageButton = withGate(
() => {
const { organizationSettings } = useEnvironment();

const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const isDomainsEnabled = organizationSettings?.domains?.enabled;

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
};
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

return <NotificationCountBadge notificationCount={membershipRequests?.count || 0} />;
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';

import { NotificationCountBadge } from '../../common';
import { NotificationCountBadge, withGate } from '../../common';
import {
useCoreOrganization,
useCoreOrganizationList,
Expand DownExpand Up@@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer(
);
}),
);
const NotificationCountBadgeSwitcherTrigger = () => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { membership } = useCoreOrganization();
const { organizationSettings } = useEnvironment();
const isAdmin = membership?.role === 'admin';
const allowRequests = organizationSettings?.domains?.enabled && isAdmin;
const { membershipRequests } = useCoreOrganization({
membershipRequests: allowRequests || undefined,
});
const NotificationCountBadgeSwitcherTrigger = withGate(
() => {
/**
* Prefetch user invitations and suggestions
*/
const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams);
const { organizationSettings } = useEnvironment();
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});

const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);
const notificationCount =
(userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0);

return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
};
return (
<NotificationCountBadge
containerSx={t => ({
marginLeft: `${t.space.$2}`,
})}
notificationCount={notificationCount}
/>
);
},
{
// if the user is not able to accept a request we should not notify them
permission: 'org:sys_memberships:manage',
},
);
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');

describe('OrganizationSwitcher', () => {
it('renders component', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button')).toBeDefined();
});

describe('Personal Workspace', () => {
it('shows the personal workspace when enabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(getByText('Personal account')).toBeDefined();
});

it('does not show the personal workspace when disabled', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand DownExpand Up@@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand DownExpand Up@@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => {
}),
);

fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);

await runFakeTimers(async () => {
const { getByText } = render(<OrganizationSwitcher />, { wrapper });

Expand All@@ -115,21 +122,23 @@ describe('OrganizationSwitcher', () => {

describe('OrganizationSwitcherPopover', () => {
it('opens the organization switcher popover when clicked', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
expect(getByText('Create Organization')).toBeDefined();
});

it('lists all organizations the user belongs to', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] });
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: false });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => {
['Member', 'basic_member'],
['Guest', 'guest_member'],
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => {
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand All@@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: true,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
Expand All@@ -189,14 +201,15 @@ describe('OrganizationSwitcher', () => {
});

it('does not display create organization button if permissions not present', async () => {
const { wrapper, props } = await createFixtures(f => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
props.setProps({ hidePersonal: true });
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
Expand All@@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => {
create_organization_enabled: false,
});
});
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
Promise.resolve({
data: [
Expand DownExpand Up@@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => {
});
});
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);

props.setProps({ hidePersonal: true });
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
Expand DownExpand Up@@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => {
});
});

fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
await userEvent.click(getByRole('button'));
Expand Down