diff --git a/.changeset/shy-pages-share.md b/.changeset/shy-pages-share.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/shy-pages-share.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.controller.ts b/packages/mosaic/src/features/organization-profile/organization-profile.controller.ts deleted file mode 100644 index 4f1542df339..00000000000 --- a/packages/mosaic/src/features/organization-profile/organization-profile.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from 'react'; - -import { organizationProfilePageIds } from './organization-profile.layout'; -import type { OrganizationProfilePageListOptions, OrganizationProfileViewProps } from './organization-profile.view'; - -type OrganizationProfileControllerOptions = Pick & { - pages?: OrganizationProfilePageListOptions['pages']; - defaultPage?: OrganizationProfileViewProps['activePage']; -}; - -export function useOrganizationProfileController({ - pages, - customPages, - pageOrder, - defaultPage, -}: OrganizationProfileControllerOptions) { - const [activePage, setActivePage] = useState( - defaultPage ?? pageOrder?.[0] ?? 'general', - ); - - if (pages) { - const pageIds = organizationProfilePageIds({ pages, customPages, pageOrder }); - const firstPage = pageIds[0]; - if (firstPage !== undefined && !pageIds.includes(activePage)) { - setActivePage(firstPage); - } - } - - return { activePage, onPageChange: setActivePage }; -} diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.layout.ts b/packages/mosaic/src/features/organization-profile/organization-profile.layout.ts index 53a103ef079..176d7bd842c 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile.layout.ts +++ b/packages/mosaic/src/features/organization-profile/organization-profile.layout.ts @@ -9,9 +9,9 @@ import { withUniquePaths } from '../profile-pages/profile-pages.layout'; import type { CustomOrganizationProfilePage, OrganizationProfilePageId, + OrganizationProfilePageListOptions, OrganizationProfilePages, } from './organization-profile.types'; -import type { OrganizationProfilePageListOptions } from './organization-profile.view'; export const ORGANIZATION_PROFILE_PAGE_IDS: readonly OrganizationProfilePageId[] = [ 'general', @@ -58,12 +58,14 @@ export function resolveOrganizationProfilePages( return applyOrder(order, entries, entry => entry.id); } -export function organizationProfilePageIds({ +export function organizationProfileNavEntries({ pages, customPages, pageOrder, -}: OrganizationProfilePageListOptions): string[] { - return resolveOrganizationProfilePages(getAvailableOrganizationProfilePages(pages), customPages, pageOrder).map( - entry => entry.id, - ); +}: OrganizationProfilePageListOptions): OrganizationProfileNavEntry[] { + return resolveOrganizationProfilePages(getAvailableOrganizationProfilePages(pages), customPages, pageOrder); +} + +export function organizationProfilePageIds(options: OrganizationProfilePageListOptions): string[] { + return organizationProfileNavEntries(options).map(entry => entry.id); } diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.tsx b/packages/mosaic/src/features/organization-profile/organization-profile.tsx index cdffb75ce62..9d736de54df 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile.tsx @@ -3,7 +3,8 @@ import type { ReactNode } from 'react'; import type { APIKeysPageProps } from '../api-keys/api-keys-table'; -import { useOrganizationProfileController } from './organization-profile.controller'; +import { useProfilePagesController } from '../profile-pages/profile-pages.controller'; +import { organizationProfilePageIds } from './organization-profile.layout'; import { useOrganizationProfileModel } from './organization-profile.model'; import type { OrganizationProfileOptions } from './organization-profile.provider'; import { OrganizationProfileProvider } from './organization-profile.provider'; @@ -88,7 +89,10 @@ export function OrganizationProfile({ }: OrganizationProfileProps) { const model = useOrganizationProfileModel(); const pages = model.status === 'ready' ? organizationProfilePanels(model.pages, apiKeysProps) : undefined; - const navigation = useOrganizationProfileController({ pages, customPages, pageOrder, defaultPage }); + const navigation = useProfilePagesController( + defaultPage ?? pageOrder?.[0] ?? 'general', + pages && organizationProfilePageIds({ pages, customPages, pageOrder }), + ); if (model.status === 'loading') { return fallback ?? null; diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.types.ts b/packages/mosaic/src/features/organization-profile/organization-profile.types.ts index d632bbd9f7e..8c421e9a5bf 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile.types.ts +++ b/packages/mosaic/src/features/organization-profile/organization-profile.types.ts @@ -17,3 +17,16 @@ export interface CustomOrganizationProfilePage { icon?: ReactNode; content: ReactNode; } + +/** What the organization's navigation is built from. */ +export interface OrganizationProfilePageListOptions { + /** The built-in pages. One left undefined is left out of the navigation. */ + pages: OrganizationProfilePages; + /** Pages of the consumer's own, added to the navigation after the built-ins. */ + customPages?: readonly CustomOrganizationProfilePage[]; + /** + * The order the navigation runs in, by id: a built-in page's id, or a custom page's `path`. Ids + * left out keep their default place behind the ones named. + */ + pageOrder?: readonly (OrganizationProfilePageId | (string & {}))[]; +} diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile.view.tsx index 9f55147fe40..7db4186450b 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile.view.tsx @@ -5,29 +5,8 @@ import { Icon } from '../../components/icon'; import { useMessages } from '../../localization'; import type { ProfilePage, ProfilePagesViewProps } from '../profile-pages/profile-pages.view'; import { ProfilePagesView } from '../profile-pages/profile-pages.view'; -import { - getAvailableOrganizationProfilePages, - ORGANIZATION_PROFILE_PAGE_ICONS, - resolveOrganizationProfilePages, -} from './organization-profile.layout'; -import type { - CustomOrganizationProfilePage, - OrganizationProfilePageId, - OrganizationProfilePages, -} from './organization-profile.types'; - -/** What the organization's navigation is built from. */ -export interface OrganizationProfilePageListOptions { - /** The built-in pages. One left undefined is left out of the navigation. */ - pages: OrganizationProfilePages; - /** Pages of the consumer's own, added to the navigation after the built-ins. */ - customPages?: readonly CustomOrganizationProfilePage[]; - /** - * The order the navigation runs in, by id: a built-in page's id, or a custom page's `path`. Ids - * left out keep their default place behind the ones named. - */ - pageOrder?: readonly (OrganizationProfilePageId | (string & {}))[]; -} +import { ORGANIZATION_PROFILE_PAGE_ICONS, organizationProfileNavEntries } from './organization-profile.layout'; +import type { OrganizationProfilePageId, OrganizationProfilePageListOptions } from './organization-profile.types'; export interface OrganizationProfileViewProps extends @@ -59,23 +38,22 @@ export const OrganizationProfileView = React.forwardRef, ): ProfilePage[] { - return resolveOrganizationProfilePages(getAvailableOrganizationProfilePages(pages), customPages, pageOrder).map( - entry => - entry.custom - ? { id: entry.id, label: entry.custom.label, icon: entry.custom.icon, content: entry.custom.content } - : { - id: entry.id, - label: labels[entry.id], - icon: ( - - ), - content: pages[entry.id], - }, + return organizationProfileNavEntries(options).map(entry => + entry.custom + ? { id: entry.id, label: entry.custom.label, icon: entry.custom.icon, content: entry.custom.content } + : { + id: entry.id, + label: labels[entry.id], + icon: ( + + ), + content: options.pages[entry.id], + }, ); } diff --git a/packages/mosaic/src/features/profile-pages/profile-pages.controller.ts b/packages/mosaic/src/features/profile-pages/profile-pages.controller.ts new file mode 100644 index 00000000000..bfd0d37f898 --- /dev/null +++ b/packages/mosaic/src/features/profile-pages/profile-pages.controller.ts @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +export function useProfilePagesController(initialPage: string, pageIds: readonly string[] | undefined) { + const [activePage, setActivePage] = useState(initialPage); + + if (pageIds) { + const [firstPage] = pageIds; + if (firstPage !== undefined && !pageIds.includes(activePage)) { + setActivePage(firstPage); + } + } + + return { activePage, onPageChange: setActivePage }; +} diff --git a/packages/mosaic/src/features/user-profile/user-profile.controller.ts b/packages/mosaic/src/features/user-profile/user-profile.controller.ts deleted file mode 100644 index 39f839a2119..00000000000 --- a/packages/mosaic/src/features/user-profile/user-profile.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useState } from 'react'; - -import { organizationProfilePageIds } from '../organization-profile/organization-profile.layout'; -import { - getAvailableUserProfilePages, - ORGANIZATION_PAGE_PREFIX, - resolveUserProfilePages, - withoutOrganizationPaths, -} from './user-profile.layout'; -import type { UserProfileViewProps } from './user-profile.view'; - -type UserProfileControllerOptions = Pick & { - initialPage: UserProfileViewProps['activePage']; - organization?: - | { status: 'loading' | 'hidden' } - | { status: 'ready'; pageList: NonNullable }; -}; - -export function useUserProfileController({ - initialPage, - pages, - customPages, - pageOrder, - organization, -}: UserProfileControllerOptions) { - const [activePage, setActivePage] = useState(initialPage); - - if (organization && organization.status !== 'loading' && activePage.startsWith(ORGANIZATION_PAGE_PREFIX)) { - const organizationPageIds = - organization.status === 'ready' - ? organizationProfilePageIds(organization.pageList).map(id => `${ORGANIZATION_PAGE_PREFIX}${id}`) - : []; - if (!organizationPageIds.includes(activePage)) { - setActivePage( - resolveUserProfilePages( - getAvailableUserProfilePages(pages), - withoutOrganizationPaths(customPages), - pageOrder, - )[0]?.id ?? 'account', - ); - } - } - - return { activePage, onPageChange: setActivePage }; -} diff --git a/packages/mosaic/src/features/user-profile/user-profile.layout.ts b/packages/mosaic/src/features/user-profile/user-profile.layout.ts index ca3265c66cd..e3df4d774f2 100644 --- a/packages/mosaic/src/features/user-profile/user-profile.layout.ts +++ b/packages/mosaic/src/features/user-profile/user-profile.layout.ts @@ -6,6 +6,8 @@ import { CreditCard } from '../../icons/glyphs/credit-card'; import { ShieldCheck } from '../../icons/glyphs/shield-check'; import { UserCircle } from '../../icons/glyphs/user-circle'; import { applyOrder } from '../../utils/apply-order'; +import { organizationProfilePageIds } from '../organization-profile/organization-profile.layout'; +import type { OrganizationProfilePageListOptions } from '../organization-profile/organization-profile.types'; import { withUniquePaths } from '../profile-pages/profile-pages.layout'; import type { CustomProfilePage, UserProfilePageId, UserProfilePages } from './user-profile.types'; @@ -58,7 +60,30 @@ export function resolveUserProfilePages( return applyOrder(order, entries, entry => entry.id); } -export function withoutOrganizationPaths(customPages: readonly CustomProfilePage[] = []): CustomProfilePage[] { +type UserProfilePageListOptions = { + pages: UserProfilePages; + customPages?: readonly CustomProfilePage[]; + pageOrder?: readonly string[]; + organization?: OrganizationProfilePageListOptions; +}; + +export function userProfileNavEntries({ + pages, + customPages, + pageOrder, +}: UserProfilePageListOptions): UserProfileNavEntry[] { + return resolveUserProfilePages(getAvailableUserProfilePages(pages), withoutOrganizationPaths(customPages), pageOrder); +} + +export function userProfilePageIds(options: UserProfilePageListOptions): string[] { + const accountPageIds = userProfileNavEntries(options).map(entry => entry.id); + const organizationPageIds = options.organization + ? organizationProfilePageIds(options.organization).map(id => `${ORGANIZATION_PAGE_PREFIX}${id}`) + : []; + return [...accountPageIds, ...organizationPageIds]; +} + +function withoutOrganizationPaths(customPages: readonly CustomProfilePage[] = []): CustomProfilePage[] { return customPages.filter(page => { if (!page.path.startsWith(ORGANIZATION_PAGE_PREFIX)) { return true; diff --git a/packages/mosaic/src/features/user-profile/user-profile.tsx b/packages/mosaic/src/features/user-profile/user-profile.tsx index f77d428ef11..99e07acb914 100644 --- a/packages/mosaic/src/features/user-profile/user-profile.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile.tsx @@ -7,9 +7,10 @@ import type { OrganizationProfileProps } from '../organization-profile/organizat import { organizationProfilePanels } from '../organization-profile/organization-profile'; import { useOrganizationProfileModel } from '../organization-profile/organization-profile.model'; import { OrganizationProfileProvider } from '../organization-profile/organization-profile.provider'; +import { useProfilePagesController } from '../profile-pages/profile-pages.controller'; import { useUserProfilePages } from '../switcher/switcher.pages'; import type { SwitcherMode } from '../switcher/switcher.types'; -import { useUserProfileController } from './user-profile.controller'; +import { userProfilePageIds } from './user-profile.layout'; import type { UserProfileOptions } from './user-profile.provider'; import { UserProfileProvider } from './user-profile.provider'; import type { UserProfileViewProps } from './user-profile.view'; @@ -174,7 +175,7 @@ type AccountViewProps = Omit) { const model = useOrganizationProfileModel(); - const organizationState = + const organization = model.status === 'ready' - ? { - status: 'ready' as const, - pageList: { pages: organizationProfilePanels(model.pages, apiKeysProps), customPages, pageOrder }, - } - : { status: model.status }; - const navigation = useUserProfileController({ + ? { pages: organizationProfilePanels(model.pages, apiKeysProps), customPages, pageOrder } + : undefined; + const navigation = useProfilePagesController( initialPage, - ...viewProps, - organization: organizationState, - }); + model.status === 'loading' ? undefined : userProfilePageIds({ ...viewProps, organization }), + ); return ( ); diff --git a/packages/mosaic/src/features/user-profile/user-profile.view.tsx b/packages/mosaic/src/features/user-profile/user-profile.view.tsx index bda54dbce02..19845659935 100644 --- a/packages/mosaic/src/features/user-profile/user-profile.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile.view.tsx @@ -3,18 +3,14 @@ import React from 'react'; import { Icon } from '../../components/icon'; import { useMessages } from '../../localization'; -import type { OrganizationProfilePageId } from '../organization-profile/organization-profile.types'; -import type { OrganizationProfilePageListOptions } from '../organization-profile/organization-profile.view'; +import type { + OrganizationProfilePageId, + OrganizationProfilePageListOptions, +} from '../organization-profile/organization-profile.types'; import { organizationProfilePageList } from '../organization-profile/organization-profile.view'; import type { ProfilePage, ProfilePageGroup, ProfilePagesViewProps } from '../profile-pages/profile-pages.view'; import { ProfilePagesView } from '../profile-pages/profile-pages.view'; -import { - getAvailableUserProfilePages, - ORGANIZATION_PAGE_PREFIX, - resolveUserProfilePages, - USER_PROFILE_PAGE_ICONS, - withoutOrganizationPaths, -} from './user-profile.layout'; +import { ORGANIZATION_PAGE_PREFIX, USER_PROFILE_PAGE_ICONS, userProfileNavEntries } from './user-profile.layout'; import type { CustomProfilePage, UserProfilePageId, UserProfilePages } from './user-profile.types'; export interface UserProfileViewProps extends Omit< @@ -53,10 +49,7 @@ export const UserProfileView = React.forwardRef, labels: Record, ): ProfilePage[] { - return resolveUserProfilePages(getAvailableUserProfilePages(pages), customPages, pageOrder).map(entry => + return userProfileNavEntries({ pages, customPages, pageOrder }).map(entry => entry.custom ? { id: entry.id, label: entry.custom.label, icon: entry.custom.icon, content: entry.custom.content } : {