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/mosaic-organization-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/mosaic': patch
Comment thread
alexcarpenter marked this conversation as resolved.
---

Rename the `userButton.workspaces` localization keys to `userButton.organizations`. Update any overrides of `userButton.workspaces.*` to use `userButton.organizations.*`.
6 changes: 3 additions & 3 deletions packages/mosaic/src/__tests__/localization.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ describe('resolveLocalization', () => {
it('expands dot paths', () => {
const { messages } = resolveLocalization({ messages: { 'userButton.popup.label': 'Konto' } });
expect(messages.userButton.popup.label).toBe('Konto');
expect(messages.userButton.workspaces.personal).toBe(mosaicMessages.userButton.workspaces.personal);
expect(messages.userButton.organizations.personal).toBe(mosaicMessages.userButton.organizations.personal);
});

it('merges plural forms per category', () => {
const { messages } = resolveLocalization({
messages: { 'userButton.workspaces.members': { few: '{count} Mitglieder' } },
messages: { 'userButton.organizations.members': { few: '{count} Mitglieder' } },
});
expect(messages.userButton.workspaces.members).toEqual({
expect(messages.userButton.organizations.members).toEqual({
one: '{count} member',
few: '{count} Mitglieder',
other: '{count} members',
Expand Down
18 changes: 9 additions & 9 deletions packages/mosaic/src/components/menu/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ function renderMenu(props?: { onSignOut?: () => void }) {
<Menu.Root>
<Menu.Trigger />
<Menu.Popup>
<Menu.Item label='Add workspace'>
<Menu.Item label='Add organization'>
<Menu.Media>
<svg data-testid='add-icon' />
</Menu.Media>
<Menu.Label>Add workspace</Menu.Label>
<Menu.Label>Add organization</Menu.Label>
</Menu.Item>
<Menu.Separator />
<Menu.Item
Expand Down Expand Up @@ -62,8 +62,8 @@ describe('Mosaic Menu', () => {
focusableWhenDisabled
/>
<Menu.Popup>
<Menu.Item label='Add workspace'>
<Menu.Label>Add workspace</Menu.Label>
<Menu.Item label='Add organization'>
<Menu.Label>Add organization</Menu.Label>
</Menu.Item>
</Menu.Popup>
</Menu.Root>,
Expand Down Expand Up @@ -96,8 +96,8 @@ describe('Mosaic Menu', () => {
Actions
</Menu.Trigger>
<Menu.Popup>
<Menu.Item label='Add workspace'>
<Menu.Label>Add workspace</Menu.Label>
<Menu.Item label='Add organization'>
<Menu.Label>Add organization</Menu.Label>
</Menu.Item>
</Menu.Popup>
</Menu.Root>,
Expand All @@ -117,7 +117,7 @@ describe('Mosaic Menu', () => {
// `role="menu"` sits on the positioner (floating-ui owns it); the popup is the surface inside.
expect(screen.getByRole('menu')).toHaveClass('cl-menu-positioner');
expect(screen.getByRole('menu').querySelector('.cl-menu-popup')).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: 'Add workspace' })).toHaveAttribute('data-color', 'neutral');
expect(screen.getByRole('menuitem', { name: 'Add organization' })).toHaveAttribute('data-color', 'neutral');
expect(screen.getByRole('separator')).toHaveClass('cl-menu-separator');
expect(screen.getByTestId('add-icon')).toBeInTheDocument();
});
Expand Down Expand Up @@ -262,11 +262,11 @@ describe('Mosaic Menu', () => {
<Menu.Root defaultOpen>
<Menu.Trigger />
<Menu.Popup>
<Menu.Item label='Add workspace'>
<Menu.Item label='Add organization'>
<Menu.Media>
<svg data-testid='add-icon' />
</Menu.Media>
<Menu.Label>Add workspace</Menu.Label>
<Menu.Label>Add organization</Menu.Label>
</Menu.Item>
</Menu.Popup>
</Menu.Root>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('UserButton', () => {
expect(within(requiredPopup()).getByText('Alice Smith')).toBeInTheDocument();
});

describe('switching workspace', () => {
describe('switching organization', () => {
it('makes the selected organization active and closes', async () => {
const { navigate } = await renderUserButton();
const user = await open();
Expand All @@ -194,7 +194,7 @@ describe('UserButton', () => {
expect(navigate).not.toHaveBeenCalled();
});

it('leaves the active organization for the personal workspace', async () => {
it('leaves the active organization for the personal account', async () => {
await renderUserButton();
const user = await open();

Expand All @@ -206,15 +206,15 @@ describe('UserButton', () => {
expect(screen.queryByRole('button', { name: 'Personal account' })).toBeNull();
});

it('drops the personal workspace where the app hides it', async () => {
it('drops the personal account where the app hides it', async () => {
await renderUserButton({ hidePersonal: true });
await open();

expect(await screen.findByRole('button', { name: 'Other' })).toBeInTheDocument();
expect(screen.queryByText('Personal account')).toBeNull();
});

it('leads with the account, and lists no personal workspace, when the instance forces an organization and none is active', async () => {
it('leads with the account, and lists no personal account, when the instance forces an organization and none is active', async () => {
await renderUserButton(
{},
signedIn({
Expand Down Expand Up @@ -708,7 +708,7 @@ describe('UserButton', () => {
expect(within(requiredPopup()).getAllByText('alice')).toHaveLength(1);
});

it('lists no workspaces and offers no way to make one', async () => {
it('lists no organizations and offers no way to make one', async () => {
await renderUserButton({ mode: 'user' });
await open();

Expand Down Expand Up @@ -774,7 +774,7 @@ describe('UserButton', () => {
expect(screen.queryByRole('button', { name: 'Manage organization' })).toBeNull();
});

it('carries no account rows, and trails the workspaces with create-organization', async () => {
it('carries no account rows, and trails the organizations with create-organization', async () => {
await renderUserButton({ mode: 'organization' }, noOffers());
await openWithList();

Expand Down Expand Up @@ -815,7 +815,7 @@ describe('UserButton', () => {
]);
});

it('trails the workspaces with create-organization', async () => {
it('trails the organizations with create-organization', async () => {
await renderUserButton({}, noOffers());
await openWithList();

Expand Down Expand Up @@ -902,8 +902,8 @@ describe('UserButton', () => {
});
});

describe('the workspace list', () => {
it('lists the active workspace first, then the rest held, then the invitations, then the suggestions', async () => {
describe('the organization list', () => {
it('lists the active organization first, then the rest held, then the invitations, then the suggestions', async () => {
await renderUserButton();
await openWithList();

Expand All @@ -916,22 +916,22 @@ describe('UserButton', () => {
]);
});

it('names the active workspace as the current one', async () => {
it('names the active organization as the current one', async () => {
await renderUserButton();
await openWithList();

expect(current()).toEqual([expect.stringContaining('Acme')]);
});

it('checks the personal workspace, and offers no switch to it, where it is active', async () => {
it('checks the personal account, and offers no switch to it, where it is active', async () => {
await renderUserButton({}, signedIn({ client: fapiClient([personalSession, bobSession]) }));
await openWithList();

expect(current()).toEqual([expect.stringContaining('Personal account')]);
expect(screen.queryByRole('button', { name: 'Personal account' })).toBeNull();
});

it('describes each offer by the workspace it acts on', async () => {
it('describes each offer by the organization it acts on', async () => {
await renderUserButton();
await openWithList();

Expand Down Expand Up @@ -982,7 +982,7 @@ describe('UserButton', () => {
expect(reading('Beta', 'Requested')).toEqual(['Beta', 'Requested']);
});

it('lists an accepted invitation once, as a workspace to switch to', async () => {
it('lists an accepted invitation once, as an organization to switch to', async () => {
await renderUserButton({}, noOffers({ invitations: [fapiInvitation('inv_1', gamma)] }));
const user = await openWithList();

Expand Down
14 changes: 7 additions & 7 deletions packages/mosaic/src/features/user-button/user-button.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import type { UserButtonData, UserButtonHeaderLayout, UserButtonMode } from './u
* │ ⤴ Sign out │ │ │ │ ⤴ Sign out │ ┘ footer
* └────────────────────────────┘ └──────────────────────────┘ └────────────────────────────┘
*
* The organizations are listed on the surface, since they are the workspaces the active account can
* The organizations are listed on the surface, since they are what the active account can
* switch between. The other signed-in accounts are not: they are one row at the foot that opens a
* flyout of them, so the surface stays about the workspace it is on.
* flyout of them, so the surface stays about the organization it is on.
*
* The header is about whatever leads, not the mode: an organization is managed and invited to, an
* account is managed, and signed out of at the foot. A combined surface always leads with the account. With an
Expand All @@ -43,7 +43,7 @@ export type UserButtonAction =
/**
* What the trigger names and the header leads with. `member` is the account, badged with the
* organization it is active in. `none` is an organization-led surface with no organization active
* and no personal workspace to fall back to.
* and no personal account to fall back to.
*/
export type UserButtonLead = 'organization' | 'member' | 'user' | 'none';

Expand All @@ -57,9 +57,9 @@ const headers = {
/** One mode's whole surface below the header, top to bottom. */
interface ModeLayout {
/**
* The workspaces the active account switches between: its own, plus the organizations it is in.
* The organizations the active account switches between: its personal account, plus the organizations it is in.
* `false` is a list the mode does not carry at all. `footer` trails the rows, inside the list,
* since what it offers is one more workspace.
* since what it offers is one more organization.
*/
organizations: { footer: readonly UserButtonAction[] } | false;
/**
Expand All @@ -79,7 +79,7 @@ const modes = {
organizations: { footer: ['createOrganization'] },
footer: { multiSession: [], singleSession: [] },
},
// No workspaces at all, so the foot is the whole of it.
// No organizations at all, so the foot is the whole of it.
user: {
organizations: false,
footer: { multiSession: ['switchAccount', 'signOut'], singleSession: ['addAccount', 'signOut'] },
Expand All @@ -92,7 +92,7 @@ const modes = {
*/
export interface UserButtonLayout {
lead: UserButtonLead;
/** The organization rows: their own workspace, the organizations, and what is on offer. */
/** The organization rows: the personal account, the organizations, and what is on offer. */
showOrganizations: boolean;
headerLayout: UserButtonHeaderLayout;
/** What each slot carries, in the order it renders. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const userButtonMessages = {
popup: {
label: 'Account',
},
workspaces: {
organizations: {
personal: 'Personal account',
notSelected: 'No organization selected',
loading: 'Loading organizations…',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export function useUserButtonModel(options?: UserButtonModelOptions, modals?: Us
: [],
);

// Organization requests are scoped to the active session, so another account's workspaces are unknowable.
// Organization requests are scoped to the active session, so another account's organizations are unknowable.
const additionalSessions: UserButtonSession[] = (clerk.client?.signedInSessions ?? []).flatMap(s => {
const sessionUser = s.user;
if (!sessionUser || s.id === session.id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export const styles = stylex.create({
minWidth: 0,
},

// Matches `UserButtonItemLabel`, so the trigger names a workspace the same way its row does. Capped,
// because the trigger sits in a host app's chrome and a long workspace name would push it apart.
// Matches `UserButtonItemLabel`, so the trigger names an organization the same way its row does. Capped,
// because the trigger sits in a host app's chrome and a long organization name would push it apart.
triggerName: {
color: colorVars['--cl-color-foreground'],
fontSize: typeScaleVars['--cl-text-sm-size'],
Expand All @@ -83,7 +83,7 @@ export const styles = stylex.create({
marginInlineEnd: space['1'],
},

// The workspace list scrolls; the header and footer stay put. The scroll area carries the
// The organization list scrolls; the header and footer stay put. The scroll area carries the
// overflow, the edge fades, the scrollbar and the scroll padding they need, so only the cap
// is ours.
scroll: {
Expand Down
16 changes: 8 additions & 8 deletions packages/mosaic/src/features/user-button/user-button.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface UserButtonInvitation {
organizationId: string;
organizationName: string;
imageUrl?: string;
/** `accepted` is already a workspace, so it lists as one rather than offering to be accepted. */
/** `accepted` is already a membership, so it lists as one rather than offering to be accepted. */
status: 'pending' | 'accepted';
}

Expand All @@ -54,22 +54,22 @@ export interface UserButtonData {
/**
* The active organization, described whole rather than found in `memberships`, so the surface
* names it while the list it belongs to is still loading. `null` means none is active: the
* personal workspace when one exists, and no selection otherwise.
* personal account when one exists, and no selection otherwise.
*/
activeOrganization: UserButtonMembership | null;
/**
* Explicit; do not derive from `memberships.length`. Answered before the lists are fetched, so
* the surface knows whether to carry a workspace section at all without waiting on them.
* the surface knows whether to carry an organization section at all without waiting on them.
*/
hasOrganizations: boolean;
/**
* The account has no workspace of its own to return to, so the organizations are all there is.
* There is no personal account to return to, so the organizations are all there is.
* Withholds the personal row rather than standing it down: this is not a switch that is
* momentarily unavailable, it is a workspace that does not exist here.
* momentarily unavailable, it is a personal account that does not exist here.
*/
hidePersonal?: boolean;
/**
* A first page is still in flight, so the workspace rows stand in as one placeholder rather than
* A first page is still in flight, so the organization rows stand in as one placeholder rather than
* appearing a list at a time.
*/
organizationsLoading?: boolean;
Expand All @@ -88,7 +88,7 @@ export interface UserButtonData {
export interface UserButtonCallbacks {
/**
* Acts on the active account; another account's organizations are unreachable until you switch.
* `null` selects the personal workspace, which is how an account leaves an organization.
* `null` selects the personal account, which is how an account leaves an organization.
*/
onSelectOrganization?: (organizationId: string | null) => void;
onAcceptSuggestion?: (suggestionId: string) => void;
Expand All @@ -111,7 +111,7 @@ export interface UserButtonCallbacks {
export type UserButtonMode = 'combined' | 'organization' | 'user';

/**
* How the header carries its actions: `inline` trails the workspace with them, the gear as an icon;
* How the header carries its actions: `inline` trails the lead with them, the gear as an icon;
* `stacked` runs them under it as full-width labelled buttons.
*/
export type UserButtonHeaderLayout = 'inline' | 'stacked';
Expand Down
Loading
Loading