diff --git a/.changeset/mosaic-organization-naming.md b/.changeset/mosaic-organization-naming.md new file mode 100644 index 00000000000..a1807bbfc3d --- /dev/null +++ b/.changeset/mosaic-organization-naming.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Rename the `userButton.workspaces` localization keys to `userButton.organizations`. Update any overrides of `userButton.workspaces.*` to use `userButton.organizations.*`. diff --git a/packages/mosaic/src/__tests__/localization.test.tsx b/packages/mosaic/src/__tests__/localization.test.tsx index 296cfd07547..31c7fa57bc7 100644 --- a/packages/mosaic/src/__tests__/localization.test.tsx +++ b/packages/mosaic/src/__tests__/localization.test.tsx @@ -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', diff --git a/packages/mosaic/src/components/menu/menu.test.tsx b/packages/mosaic/src/components/menu/menu.test.tsx index e7711c47dbd..61bab15e268 100644 --- a/packages/mosaic/src/components/menu/menu.test.tsx +++ b/packages/mosaic/src/components/menu/menu.test.tsx @@ -24,11 +24,11 @@ function renderMenu(props?: { onSignOut?: () => void }) { - + - Add workspace + Add organization { focusableWhenDisabled /> - - Add workspace + + Add organization , @@ -96,8 +96,8 @@ describe('Mosaic Menu', () => { Actions - - Add workspace + + Add organization , @@ -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(); }); @@ -262,11 +262,11 @@ describe('Mosaic Menu', () => { - + - Add workspace + Add organization , diff --git a/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx b/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx index 08657d42c66..70c4d37400b 100644 --- a/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx +++ b/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx @@ -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(); @@ -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(); @@ -206,7 +206,7 @@ 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(); @@ -214,7 +214,7 @@ describe('UserButton', () => { 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({ @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -916,14 +916,14 @@ 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(); @@ -931,7 +931,7 @@ describe('UserButton', () => { 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(); @@ -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(); diff --git a/packages/mosaic/src/features/user-button/user-button.layout.ts b/packages/mosaic/src/features/user-button/user-button.layout.ts index bb3743f7f1c..346142dd0f9 100644 --- a/packages/mosaic/src/features/user-button/user-button.layout.ts +++ b/packages/mosaic/src/features/user-button/user-button.layout.ts @@ -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 @@ -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'; @@ -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; /** @@ -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'] }, @@ -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. */ diff --git a/packages/mosaic/src/features/user-button/user-button.messages.ts b/packages/mosaic/src/features/user-button/user-button.messages.ts index 10f236ce4f9..f08288a4c54 100644 --- a/packages/mosaic/src/features/user-button/user-button.messages.ts +++ b/packages/mosaic/src/features/user-button/user-button.messages.ts @@ -5,7 +5,7 @@ export const userButtonMessages = { popup: { label: 'Account', }, - workspaces: { + organizations: { personal: 'Personal account', notSelected: 'No organization selected', loading: 'Loading organizations…', diff --git a/packages/mosaic/src/features/user-button/user-button.model.tsx b/packages/mosaic/src/features/user-button/user-button.model.tsx index b987d404d33..3c8e3739e97 100644 --- a/packages/mosaic/src/features/user-button/user-button.model.tsx +++ b/packages/mosaic/src/features/user-button/user-button.model.tsx @@ -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) { diff --git a/packages/mosaic/src/features/user-button/user-button.styles.ts b/packages/mosaic/src/features/user-button/user-button.styles.ts index 7b58a96458b..29dce5c1688 100644 --- a/packages/mosaic/src/features/user-button/user-button.styles.ts +++ b/packages/mosaic/src/features/user-button/user-button.styles.ts @@ -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'], @@ -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: { diff --git a/packages/mosaic/src/features/user-button/user-button.types.ts b/packages/mosaic/src/features/user-button/user-button.types.ts index 4919aa512d9..a715f6bffdb 100644 --- a/packages/mosaic/src/features/user-button/user-button.types.ts +++ b/packages/mosaic/src/features/user-button/user-button.types.ts @@ -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'; } @@ -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; @@ -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; @@ -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'; diff --git a/packages/mosaic/src/features/user-button/user-button.view.tsx b/packages/mosaic/src/features/user-button/user-button.view.tsx index d7755ae6814..51791d865c7 100644 --- a/packages/mosaic/src/features/user-button/user-button.view.tsx +++ b/packages/mosaic/src/features/user-button/user-button.view.tsx @@ -98,7 +98,7 @@ function useBusy(key?: string): { busy: boolean; disabled: boolean } { return { busy: pendingKey === key, disabled: pendingKey !== key }; } -type ActiveWorkspace = +type LeadSelection = | { kind: 'organization'; name: string; @@ -111,14 +111,14 @@ type ActiveWorkspace = /** * What the surface leads with: named in the trigger and headed in the popup, so the two always - * agree. An organization-led surface with no org and no personal workspace is no selection. + * agree. An organization-led surface with no org and no personal account is no selection. */ type Messages = MosaicMessages['userButton']; -function leadWorkspace( +function leadSelection( { layout, activeOrganization, activeSession }: UserButtonContextValue, m: Messages, -): ActiveWorkspace { +): LeadSelection { if (layout.lead === 'organization' && activeOrganization) { return { kind: 'organization', @@ -129,7 +129,7 @@ function leadWorkspace( }; } if (layout.lead === 'none') { - return { kind: 'none', name: m.workspaces.notSelected, shape: 'square' }; + return { kind: 'none', name: m.organizations.notSelected, shape: 'square' }; } return { kind: 'user', @@ -140,12 +140,12 @@ function leadWorkspace( }; } -/** The organization a workspace shows: the one it is, or the one badging the account. */ -function workspaceOrganization(workspace: ActiveWorkspace): UserButtonMembership | undefined { - if (workspace.kind === 'organization') { - return workspace.organization; +/** The organization a lead shows: the one it is, or the one badging the account. */ +function leadOrganization(lead: LeadSelection): UserButtonMembership | undefined { + if (lead.kind === 'organization') { + return lead.organization; } - return workspace.kind === 'user' ? workspace.badge : undefined; + return lead.kind === 'user' ? lead.badge : undefined; } function joinDetails(...parts: Array): string { @@ -154,7 +154,9 @@ function joinDetails(...parts: Array): string { function membershipSubtitle(membership: UserButtonMembership, m: Messages, locale: string): string { const members = - membership.membersCount === undefined ? undefined : plural(m.workspaces.members, membership.membersCount, locale); + membership.membersCount === undefined + ? undefined + : plural(m.organizations.members, membership.membersCount, locale); return joinDetails(membership.planLabel, members); } @@ -188,7 +190,7 @@ function PendingSpinner() { ); @@ -205,7 +207,7 @@ interface SwitcherRowProps { avatarName?: string; imageUrl?: string; shape: 'circle' | 'square'; - /** Names the title element, for a control in the row that has to point at the workspace it acts on. */ + /** Names the title element, for a control in the row that has to point at the organization it acts on. */ labelId?: string; active?: boolean; onSelect?: () => void; @@ -415,7 +417,7 @@ function HeaderActionButton({ onClick={onClick} type='button' isPending={busy} - pendingLabel={m.workspaces.pending} + pendingLabel={m.organizations.pending} spinDelay={{ delay: 0 }} > {leading} @@ -424,7 +426,7 @@ function HeaderActionButton({ ); } -/** The active workspace: who you are signed in as, and what you can do about it. */ +/** The lead: who you are signed in as, and what you can do about it. */ function Header() { const m = useMessages('userButton'); const locale = useLocale(); @@ -432,15 +434,15 @@ function Header() { const layout = data.layout.headerLayout; const userLed = data.layout.lead === 'user'; const { identifier } = data.activeSession; - const workspace = leadWorkspace(data, m); - const { name } = workspace; - const organization = workspaceOrganization(workspace); + const lead = leadSelection(data, m); + const { name } = lead; + const organization = leadOrganization(lead); // An account with no name is titled by its identifier, and repeating it underneath says nothing. // No selection is not the account, so it carries no identifier line either. const subtitle = - workspace.kind === 'organization' - ? membershipSubtitle(workspace.organization, m, locale) - : workspace.kind === 'user' && identifier !== name + lead.kind === 'organization' + ? membershipSubtitle(lead.organization, m, locale) + : lead.kind === 'user' && identifier !== name ? identifier : ''; @@ -463,7 +465,7 @@ function Header() { onClick: data.onManageOrganization, }); } - if (workspace.kind !== 'organization' && data.onManageAccount) { + if (lead.kind !== 'organization' && data.onManageAccount) { settings.push({ name: m.manage.account, label: m.manage.profileSettings, @@ -497,11 +499,11 @@ function Header() { layout={layout} avatar={ } title={name} @@ -545,7 +547,7 @@ function MembershipRow({ membership, active, onSelect }: MembershipRowProps) { } /** - * The account's own workspace, which is what "no active organization" is. Listed alongside the + * The personal account, which is what "no active organization" is. Listed alongside the * organizations so switching into one is not a one-way door: `null` is how you leave. * * Named for what it is among organizations rather than for the account, the way the existing @@ -564,7 +566,7 @@ function PersonalRow() { return ( m.organizationId)); @@ -690,7 +692,7 @@ function PendingRows() { {/* Invitations first: one is addressed to this account and joins on accept, where a suggestion only files a request. Same order as the existing OrganizationSwitcher. */} {invitations.map(i => - // Already joined, so it is a workspace like any other: click the row to switch to it. + // Already joined, so it is an organization like any other: click the row to switch to it. i.status === 'accepted' ? ( acceptInvitation(i.id) : undefined} /> ), @@ -720,9 +722,9 @@ function PendingRows() { busyKey={userButtonBusyKeys.acceptSuggestion(s.id)} name={s.name} imageUrl={s.imageUrl} - actionLabel={m.workspaces.join} + actionLabel={m.organizations.join} // An accepted suggestion is waiting on an admin, so it reports rather than re-offers. - note={s.status === 'accepted' ? m.workspaces.requested : undefined} + note={s.status === 'accepted' ? m.organizations.requested : undefined} onAccept={acceptSuggestion ? () => acceptSuggestion(s.id) : undefined} /> ))} @@ -732,7 +734,7 @@ function PendingRows() { /** * A signed-in account inside the accounts flyout: a menu item you pick to switch to, checked where - * it is already the active one. Its workspaces cannot be listed here — they are scoped to the + * it is already the active one. Its organizations cannot be listed here — they are scoped to the * session that fetches them — so switching is all it offers. */ function SessionMenuItem({ session, active }: { session: UserButtonSession; active: boolean }) { @@ -885,13 +887,13 @@ function OrganizationListLoadingRow() { - {m.workspaces.loading} + {m.organizations.loading} ); } -/** The workspaces the active account can switch between. This is the group that scrolls. */ +/** The organizations the active account can switch between. This is the group that scrolls. */ function OrganizationSection() { const m = useMessages('userButton'); const data = useUserButtonContext(); @@ -943,7 +945,7 @@ function OrganizationSection() { ))} {/* Trails the rows rather than sitting at the foot of the surface: what it offers is one - more of the workspaces above it, not an action on the account. */} + more of the organizations above it, not an action on the account. */} {actions} @@ -1075,7 +1077,7 @@ export function UserButtonRoot(props: UserButtonRootProps): ReactElement { export interface UserButtonTriggerProps { /** - * Names the active workspace beside its avatar — the organization wherever one heads the + * Names the lead beside its avatar — the organization wherever one heads the * trigger, no selection when personal is hidden and none is active, the account otherwise, with * its active organization beneath it. Turn it off for the avatar alone. * @@ -1091,17 +1093,17 @@ export interface UserButtonTriggerProps { renderTriggerBadge?: boolean; } -/** The trigger: the active workspace's avatar, and what it is called. */ +/** The trigger: the lead's avatar, and what it is called. */ export function UserButtonTrigger({ renderTriggerLabel = true, renderTriggerBadge = true, }: UserButtonTriggerProps = {}): ReactElement { const m = useMessages('userButton'); const data = useUserButtonContext(); - const workspace = leadWorkspace(data, m); - const { name, shape } = workspace; - const planLabel = renderTriggerBadge ? workspaceOrganization(workspace)?.planLabel : undefined; - const badge = workspace.kind === 'user' ? workspace.badge : undefined; + const lead = leadSelection(data, m); + const { name, shape } = lead; + const planLabel = renderTriggerBadge ? leadOrganization(lead)?.planLabel : undefined; + const badge = lead.kind === 'user' ? lead.badge : undefined; const ringsAvatar = !renderTriggerLabel && badge !== undefined; return ( @@ -1116,9 +1118,9 @@ export function UserButtonTrigger({ ]} > { test('accepts nested overrides', () => { const value: MosaicCatalog = { - userButton: { trigger: { open: 'Menü für {name}' }, workspaces: { members: { few: '{count} Mitglieder' } } }, + userButton: { trigger: { open: 'Menü für {name}' }, organizations: { members: { few: '{count} Mitglieder' } } }, }; void value; }); @@ -18,7 +18,7 @@ describe('MosaicCatalog', () => { test('accepts dot paths, mixed with nested', () => { const value: MosaicCatalog = { 'userButton.popup.label': 'Konto', - 'userButton.workspaces.members': { many: '{count} членов' }, + 'userButton.organizations.members': { many: '{count} членов' }, userProfile: { label: 'Profil' }, }; void value; @@ -53,7 +53,7 @@ describe('MosaicCatalog', () => { }; const nested: MosaicCatalog = { // @ts-expect-error plural forms cannot become a string - userButton: { workspaces: { members: 'x' } }, + userButton: { organizations: { members: 'x' } }, }; void [value, nested]; }); diff --git a/packages/swingset/src/stories/action-bar.stories.tsx b/packages/swingset/src/stories/action-bar.stories.tsx index 3a45c541c85..2b9efb5a465 100644 --- a/packages/swingset/src/stories/action-bar.stories.tsx +++ b/packages/swingset/src/stories/action-bar.stories.tsx @@ -156,7 +156,7 @@ function MembersProfile({ value='members' {...props} > - Workspace + Organization (null); return ( - }>Manage workspace + }>Manage organization ) { return ( ); } @@ -41,10 +41,10 @@ export function Default(props: Record) { export function Inline(props: Record) { return (
- clerkWorkspace-177654156132154 + clerkOrganization-177654156132154
diff --git a/packages/swingset/src/stories/localization.mdx b/packages/swingset/src/stories/localization.mdx index 5ed13d582e7..2605118b503 100644 --- a/packages/swingset/src/stories/localization.mdx +++ b/packages/swingset/src/stories/localization.mdx @@ -86,7 +86,7 @@ is left unused, and a placeholder the values do not cover is rendered literally. { "userButton": { "trigger": { "open": "Abrir el menú de la cuenta de {name}" }, - "workspaces": { + "organizations": { "members": { "one": "{count} miembro", "other": "{count} miembros" } } }, @@ -167,7 +167,7 @@ function Trigger({ name, count }: { name: string; count: number }) { const locale = useLocale(); return ( ); } diff --git a/packages/swingset/src/stories/localization.stories.tsx b/packages/swingset/src/stories/localization.stories.tsx index 3d88870ba96..22817c083fe 100644 --- a/packages/swingset/src/stories/localization.stories.tsx +++ b/packages/swingset/src/stories/localization.stories.tsx @@ -24,7 +24,7 @@ const esES: MosaicCatalog = { userButton: { trigger: { open: 'Abrir el menú de la cuenta de {name}' }, popup: { label: 'Cuenta' }, - workspaces: { + organizations: { personal: 'Cuenta personal', notSelected: 'Ninguna organización seleccionada', loading: 'Cargando organizaciones…', @@ -83,7 +83,7 @@ const clerkCloud = { // A static user button: every string it renders comes from the `userButton` namespace, so it // shows a catalog or an override the moment the popup opens. -function Workspace() { +function OrganizationUserButton() { const [open, setOpen] = useState(false); return ( 30 } })], ]} /> @@ -145,7 +145,7 @@ function Strings() { function Counts({ counts }: { counts: number[] }) { const m = useMessages('userButton'); const locale = useLocale(); - return [String(count), plural(m.workspaces.members, count, locale)])} />; + return [String(count), plural(m.organizations.members, count, locale)])} />; } export function Overrides() { @@ -158,7 +158,7 @@ export function Overrides() { }, }} > - + ); } @@ -166,7 +166,7 @@ export function Overrides() { export function Catalog() { return ( - + ); } @@ -180,7 +180,7 @@ export function CatalogWithOverrides() { overrides: { userButton: { manage: { invite: 'Invitar al equipo' } } }, }} > - + ); } @@ -203,7 +203,7 @@ export function PluralRules() { localization={{ locale: 'ru', overrides: { - 'userButton.workspaces.members': { + 'userButton.organizations.members': { one: '{count} участник', few: '{count} участника', many: '{count} участников', diff --git a/packages/swingset/src/stories/menu.component.mdx b/packages/swingset/src/stories/menu.component.mdx index fe1b5d658a6..a5f1862dd7d 100644 --- a/packages/swingset/src/stories/menu.component.mdx +++ b/packages/swingset/src/stories/menu.component.mdx @@ -20,11 +20,11 @@ import { Menu } from '@clerk/mosaic/components/menu'; - + - Add workspace + Add organization - + - Add workspace + Add organization @@ -83,7 +83,7 @@ export function Accounts() { ); } -const workspaces = [ +const organizations = [ 'Ashgrove', 'Bramblewood', 'Cedar Hollow', @@ -154,22 +154,22 @@ const workspaces = [ export function Overflowing() { return ( - Switch workspace + Switch organization - {workspaces.map(workspace => ( + {organizations.map(organization => ( - {workspace[0]} + {organization[0]} - {workspace} + {organization} ))} diff --git a/packages/swingset/src/stories/user-button.mdx b/packages/swingset/src/stories/user-button.mdx index 8aa7f9f4864..9879dd2dd6d 100644 --- a/packages/swingset/src/stories/user-button.mdx +++ b/packages/swingset/src/stories/user-button.mdx @@ -3,15 +3,15 @@ import * as UserButtonStories from './user-button.stories'; # UserButton The account & organization switcher behind the user avatar. The account heads the surface, badged -with the active organization, carrying **Settings** and **Invite**. The account's workspaces sit under it: every organization, plus +with the active organization, carrying **Settings** and **Invite**. The account's organizations sit under it: every one it belongs to, plus **suggested** and **invited** ones it can Join. **Switch account** at the foot opens the signed-in -accounts as a flyout — the active one checked — so an account never reads as a workspace. The +accounts as a flyout — the active one checked — so an account never reads as an organization. The flyout also adds an account and signs out of all of them. With nobody to switch to, that row is **Add account** instead. The foot signs out of the active account. `mode` narrows all of it; see [Modes](#modes). Only the active account's organizations are listed. Not a design choice: org requests are scoped to -the session that makes them, so another account's workspaces are unknowable until it _is_ active. +the session that makes them, so another account's organizations are unknowable until it _is_ active. Hence `additionalSessions` carries sessions alone, and `onSelectOrganization` takes no session. Composed from `Popover`, `Card`, `Item`, `Avatar`, and `Menu`, so behavior, focus, and ARIA come from @@ -21,7 +21,7 @@ it with `useUserButtonController()` and needs no props. ## Example A working prototype over mock accounts — try it. Nothing is instant: the clicked row spins, the rest -stand down, and the surface stays open so you land on the result. Only picking a workspace closes it. +stand down, and the surface stays open so you land on the result. Only picking an organization closes it. `pendingKey` drives that, and the connected `UserButton` sets it from the action in flight. Actions that would navigate (Manage, Invite, Create organization, Add account) only close the popover here. @@ -97,7 +97,7 @@ fully to match it; see [User only](#user-only). The header names whatever leads and carries what you can do about it, stacked under it as full-width labelled buttons. An organization is described by its plan and member count, and carries **Settings** with **Invite**. An account is described by its identifier. Where **Settings** is the only -action, as it is for an account, it trails the workspace inline. +action, as it is for an account, it trails the account inline. With an organization active in `combined`, **Settings** opens a menu of **Organization settings** and **Profile settings**. Where only one of them is available, **Settings** opens it directly. @@ -117,7 +117,7 @@ What a mode leads with is named in the trigger and heads the popup — the two a ### Combined -The account heads it, badged with the active org, with **Settings** and **Invite**. The workspaces +The account heads it, badged with the active org, with **Settings** and **Invite**. The organizations sit under it, with **Create organization** at their foot. **Switch account** opens the signed-in accounts, with **Add account** and **Sign out of all accounts**, and **Sign out** at the foot signs out of the active account. @@ -141,7 +141,7 @@ With one account signed in, the foot is **Add account** and **Sign out**. ### Org only -The org heads it, with **Settings** and **Invite**. **Create organization** trails the workspaces, and +The org heads it, with **Settings** and **Invite**. **Create organization** trails the organizations, and nothing else is on offer, so the popup ends there. ; +type AccountOrganizations = Omit; /** The multi-account frames: NestLabs Creative active, beside Acme, with Clerk on offer. */ -const nestLabsWithSuggestion: Workspaces = { +const nestLabsWithSuggestion: AccountOrganizations = { activeOrganizationId: nestLabs.organizationId, memberships: [nestLabs, acme], suggestions: [clerkSuggestion], @@ -93,25 +93,25 @@ const nestLabsWithSuggestion: Workspaces = { }; /** The single-account and organization-only frames: NestLabs Creative active, beside Clerk App and Acme. */ -const nestLabsWithClerkApp: Workspaces = { +const nestLabsWithClerkApp: AccountOrganizations = { activeOrganizationId: nestLabs.organizationId, memberships: [nestLabs, clerkApp, acme], suggestions: [], invitations: [], }; -/** The personal-workspace frame: no organization active, Clerk App and Acme to switch to. */ -const personalWorkspace: Workspaces = { +/** The personal-account frame: no organization active, Clerk App and Acme to switch to. */ +const personalAccount: AccountOrganizations = { activeOrganizationId: null, memberships: [clerkApp, acme], suggestions: [], invitations: [], }; -// The other two sign-ins' workspaces, reached only by switching to them. +// The other two sign-ins' organizations, reached only by switching to them. const otherAccounts: Account[] = [ { session: cameron('sess_cameron_2'), ...nestLabsWithClerkApp }, - { session: cameron('sess_cameron_3'), ...personalWorkspace }, + { session: cameron('sess_cameron_3'), ...personalAccount }, ]; function chaosAccounts(accounts: Account[]): Account[] { @@ -129,7 +129,7 @@ function chaosAccounts(accounts: Account[]): Account[] { })); } -/** Joining is what turns a suggestion or an invitation into a workspace you can switch to. */ +/** Joining is what turns a suggestion or an invitation into an organization you can switch to. */ function join(account: Account, organizationId: string, name: string, imageUrl?: string): Account { return { ...account, @@ -144,26 +144,26 @@ function join(account: Account, organizationId: string, name: string, imageUrl?: const LATENCY_MS = 800; /** - * The examples are prototypes, not screenshots: every row is wired to state, so picking a workspace - * or an account really switches to it, and Join turns a suggestion into a workspace. None of it is + * The examples are prototypes, not screenshots: every row is wired to state, so picking an organization + * or an account really switches to it, and Join turns a suggestion into an organization. None of it is * instant — each action is a network round trip against Clerk, so the prototype fakes one: the * clicked row spins, the rest stand down, and the surface stays open so you land back on the result. - * Only picking a workspace closes it, because that is the one action the surface exists to perform. + * Only picking an organization closes it, because that is the one action the surface exists to perform. * The actions that would navigate somewhere in a real app (Manage, Invite, Create organization, Add * account) have nowhere to go here, so they only close the popover. */ function usePrototype({ - workspaces = nestLabsWithSuggestion, + organizations = nestLabsWithSuggestion, singleSession = false, hidePersonal = false, }: { - workspaces?: Workspaces; + organizations?: AccountOrganizations; singleSession?: boolean; hidePersonal?: boolean; } = {}): Omit { const [open, setOpen] = useState(false); const seed = useChaosFixture( - [{ session: cameron('sess_cameron_1'), ...workspaces }, ...(singleSession ? [] : otherAccounts)], + [{ session: cameron('sess_cameron_1'), ...organizations }, ...(singleSession ? [] : otherAccounts)], chaosAccounts, ); const [accounts, setAccounts] = useState(seed); @@ -280,7 +280,7 @@ export function CombinedIconTrigger(_args: Record) { } export function CombinedSingleAccount(_args: Record) { - const prototype = usePrototype({ workspaces: nestLabsWithClerkApp, singleSession: true, hidePersonal: true }); + const prototype = usePrototype({ organizations: nestLabsWithClerkApp, singleSession: true, hidePersonal: true }); return ( ) { // ─── Mode: Org only ───────────────────────────────────────────────────────── export function Organizations(_args: Record) { - const prototype = usePrototype({ workspaces: nestLabsWithClerkApp, hidePersonal: true }); + const prototype = usePrototype({ organizations: nestLabsWithClerkApp, hidePersonal: true }); return ( ) { } export function OrganizationsIconTrigger(_args: Record) { - const prototype = usePrototype({ workspaces: nestLabsWithClerkApp, hidePersonal: true }); + const prototype = usePrototype({ organizations: nestLabsWithClerkApp, hidePersonal: true }); return ( ) { } export function NoOrganizationSelected(_args: Record) { - const prototype = usePrototype({ workspaces: personalWorkspace, hidePersonal: true }); + const prototype = usePrototype({ organizations: personalAccount, hidePersonal: true }); // Personal is withheld and nothing is active, so the lead is no selection — not the account. // Picking an organization leaves it. @@ -447,7 +447,7 @@ export function CustomMenuItems(_args: Record) { const ORGANIZATIONS_LATENCY_MS = 2500; /** - * The workspace list's first page landing under a popup that is already open. The other examples + * The organization list's first page landing under a popup that is already open. The other examples * are handed their organizations on the first render, so the placeholder they would show has * nothing to show it. The wait restarts on every open, so it can be watched more than once. */