Update OAuth consent modal design (#20540)
## Summary Updates the OAuth consent screen to match the provided modal treatment, including the header artwork, app-to-Twenty logo layout, scope icons, and a content-hugging title that stays on one line until its max width. Also preserves `returnToPath` through Google and Microsoft social SSO so users who sign in from `/authorize` return to the OAuth consent flow with the original OAuth query parameters intact. ## Reference  ## Validation - `yarn nx test twenty-front --testFile=src/modules/auth/hooks/__tests__/useAuth.test.tsx --coverage=false` - `yarn nx test twenty-server --testFile=src/engine/core-modules/auth/services/auth.service.spec.ts --coverage=false` - `yarn nx typecheck twenty-front` - `yarn nx typecheck twenty-server` - `git diff --check` ## Notes The reference screenshot is linked from a separate branch-hosted image commit so it renders in the PR body without adding that screenshot to the product diff. Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
91b2390ee8
commit
8dd0aa46e3
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -1,5 +1,5 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
|
||||
@@ -8,94 +8,200 @@ import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useQuery, useMutation } from '@apollo/client/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import {
|
||||
Avatar,
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
IconDatabase,
|
||||
IconRefresh,
|
||||
IconUserCircle,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { ModalContent } from 'twenty-ui/layout';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
AuthorizeAppDocument,
|
||||
FindApplicationRegistrationByClientIdDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAppsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledText = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: 'Inter';
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
padding: ${themeCssVariables.spacing[6]} 0px;
|
||||
`;
|
||||
|
||||
const StyledCardWrapper = styled.div`
|
||||
align-items: center;
|
||||
--oauth-modal-content-max-width: calc(
|
||||
${themeCssVariables.modal.size.md.width} + ${themeCssVariables.spacing[32]}
|
||||
);
|
||||
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: min(
|
||||
100%,
|
||||
calc(
|
||||
var(--oauth-modal-content-max-width) + ${themeCssVariables.spacing[20]}
|
||||
)
|
||||
);
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
background-image: url('/images/integrations/oauth-modal-header.png');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
height: ${themeCssVariables.spacing[30]};
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[6]};
|
||||
width: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAppLogoTile = styled.div`
|
||||
align-items: center;
|
||||
backdrop-filter: ${themeCssVariables.blur.strong};
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[12]};
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
width: ${themeCssVariables.spacing[12]};
|
||||
`;
|
||||
|
||||
const StyledAppLogo = styled.img`
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
height: ${themeCssVariables.spacing[10]};
|
||||
object-fit: cover;
|
||||
width: ${themeCssVariables.spacing[10]};
|
||||
`;
|
||||
|
||||
const StyledLinkIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.rounded};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
justify-content: center;
|
||||
width: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledOAuthTitle = styled(H1Title)`
|
||||
margin: 0;
|
||||
max-width: min(100%, var(--oauth-modal-content-max-width));
|
||||
padding-bottom: ${themeCssVariables.spacing[1]};
|
||||
text-wrap: balance;
|
||||
width: max-content;
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
grid-template-columns: repeat(
|
||||
2,
|
||||
minmax(${themeCssVariables.spacing[0]}, 1fr)
|
||||
);
|
||||
margin-top: ${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCancelLinkContainer = styled.div`
|
||||
min-width: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledAuthorizeButton = styled(MainButton)`
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
const StyledPermissionSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: ${themeCssVariables.spacing[6]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPermissionIntro = styled.p`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
line-height: ${themeCssVariables.text.lineHeight.lg};
|
||||
margin: 0;
|
||||
padding: 0 0 ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledScopeList = styled.ul`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
margin: 0 0 ${themeCssVariables.spacing[4]} 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledScopeItem = styled.li`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
padding: ${themeCssVariables.spacing[1]} 0;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
line-height: ${themeCssVariables.text.lineHeight.lg};
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
& + & {
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
padding-top: ${themeCssVariables.spacing[3]};
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledScopeIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.color.blue};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledErrorText = styled.div`
|
||||
color: ${themeCssVariables.color.red};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const OAUTH_SCOPE_ICONS: { [scope: string]: IconComponent | undefined } = {
|
||||
api: IconDatabase,
|
||||
profile: IconUserCircle,
|
||||
};
|
||||
|
||||
export const Authorize = () => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const navigate = useNavigateApp();
|
||||
const [searchParam] = useSearchParams();
|
||||
const { redirect } = useRedirect();
|
||||
|
||||
const oauthScopeLabels: { [scope: string]: string | undefined } = {
|
||||
api: t`Access workspace data`,
|
||||
api: t`Access your workspace data`,
|
||||
profile: t`Read your profile`,
|
||||
};
|
||||
|
||||
@@ -158,16 +264,19 @@ export const Authorize = () => {
|
||||
|
||||
if (isDefined(queryError)) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
||||
<StyledCardWrapper>
|
||||
<StyledText>
|
||||
<Trans>Something went wrong</Trans>
|
||||
</StyledText>
|
||||
<StyledErrorText>
|
||||
{t`Unable to load application details. Please try again later.`}
|
||||
</StyledErrorText>
|
||||
<ModalContent contentPadding={10}>
|
||||
<StyledOAuthTitle
|
||||
title={<Trans>Something went wrong</Trans>}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
<StyledErrorText>
|
||||
{t`Unable to load application details. Please try again later.`}
|
||||
</StyledErrorText>
|
||||
</ModalContent>
|
||||
</StyledCardWrapper>
|
||||
</StyledContainer>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,69 +291,87 @@ export const Authorize = () => {
|
||||
const showLogoImage = isNonEmptyString(appLogoUrl) && !hasLogoError;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
||||
<StyledCardWrapper>
|
||||
<StyledAppsContainer>
|
||||
<img
|
||||
src="/images/integrations/twenty-logo.svg"
|
||||
alt="twenty-icon"
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<img
|
||||
src="/images/integrations/link-apps.svg"
|
||||
alt="link-icon"
|
||||
height={60}
|
||||
width={60}
|
||||
/>
|
||||
{showLogoImage ? (
|
||||
<img
|
||||
src={appLogoUrl}
|
||||
alt={appName}
|
||||
height={40}
|
||||
width={40}
|
||||
style={{ borderRadius: '2px' }}
|
||||
onError={() => setHasLogoError(true)}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
size="xl"
|
||||
placeholder={appName}
|
||||
placeholderColorSeed={appName}
|
||||
type="squared"
|
||||
<StyledHeader>
|
||||
<StyledAppLogoTile>
|
||||
<StyledAppLogo src="/images/integrations/twenty-logo.svg" alt="" />
|
||||
</StyledAppLogoTile>
|
||||
<StyledLinkIconContainer aria-hidden>
|
||||
<IconRefresh
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledLinkIconContainer>
|
||||
<StyledAppLogoTile>
|
||||
{showLogoImage ? (
|
||||
<StyledAppLogo
|
||||
src={appLogoUrl}
|
||||
alt=""
|
||||
onError={() => setHasLogoError(true)}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
size="xl"
|
||||
placeholder={appName}
|
||||
placeholderColorSeed={appName}
|
||||
type="squared"
|
||||
/>
|
||||
)}
|
||||
</StyledAppLogoTile>
|
||||
</StyledHeader>
|
||||
<ModalContent contentPadding={10}>
|
||||
<StyledOAuthTitle
|
||||
title={<Trans>Connect {appName} to your account</Trans>}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
{requestedScopes.length > 0 && (
|
||||
<StyledPermissionSection>
|
||||
<StyledPermissionIntro>
|
||||
<Trans>{appName} would like to:</Trans>
|
||||
</StyledPermissionIntro>
|
||||
<StyledScopeList>
|
||||
{requestedScopes.map((scope) => {
|
||||
const ScopeIcon = OAUTH_SCOPE_ICONS[scope] ?? IconDatabase;
|
||||
|
||||
return (
|
||||
<StyledScopeItem key={scope}>
|
||||
<StyledScopeIcon>
|
||||
<ScopeIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledScopeIcon>
|
||||
<span>{oauthScopeLabels[scope] ?? scope}</span>
|
||||
</StyledScopeItem>
|
||||
);
|
||||
})}
|
||||
</StyledScopeList>
|
||||
</StyledPermissionSection>
|
||||
)}
|
||||
</StyledAppsContainer>
|
||||
<StyledText>
|
||||
<Trans>{appName} wants to access your account</Trans>
|
||||
</StyledText>
|
||||
{requestedScopes.length > 0 && (
|
||||
<StyledScopeList>
|
||||
{requestedScopes.map((scope) => (
|
||||
<StyledScopeItem key={scope}>
|
||||
{oauthScopeLabels[scope] ?? scope}
|
||||
</StyledScopeItem>
|
||||
))}
|
||||
</StyledScopeList>
|
||||
)}
|
||||
{authorizeError && <StyledErrorText>{authorizeError}</StyledErrorText>}
|
||||
<StyledButtonContainer>
|
||||
<UndecoratedLink to={AppPath.Index}>
|
||||
<MainButton
|
||||
title={t`Cancel`}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
{authorizeError && (
|
||||
<StyledErrorText>{authorizeError}</StyledErrorText>
|
||||
)}
|
||||
<StyledButtonContainer>
|
||||
<StyledCancelLinkContainer>
|
||||
<UndecoratedLink to={AppPath.Index} fullWidth>
|
||||
<MainButton
|
||||
title={t`Cancel`}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
disabled={isAuthorizing}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledCancelLinkContainer>
|
||||
<StyledAuthorizeButton
|
||||
title={isAuthorizing ? t`Authorizing...` : t`Authorize`}
|
||||
onClick={handleAuthorize}
|
||||
disabled={isAuthorizing}
|
||||
fullWidth
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
<MainButton
|
||||
title={isAuthorizing ? t`Authorizing...` : t`Authorize`}
|
||||
onClick={handleAuthorize}
|
||||
disabled={isAuthorizing}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</StyledButtonContainer>
|
||||
</ModalContent>
|
||||
</StyledCardWrapper>
|
||||
</StyledContainer>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { within } from 'storybook/test';
|
||||
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { Authorize } from '~/pages/auth/Authorize';
|
||||
import {
|
||||
PageDecorator,
|
||||
type PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getOperationName } from '~/utils/getOperationName';
|
||||
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { FindApplicationRegistrationByClientIdDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
const MOCKED_REDIRECT_URL = 'https://chatgpt.com/connector';
|
||||
|
||||
const buildHandlers = (
|
||||
application: {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
oAuthScopes: string[];
|
||||
websiteUrl: string | null;
|
||||
} | null,
|
||||
) => [
|
||||
graphql.query(
|
||||
getOperationName(FindApplicationRegistrationByClientIdDocument) ?? '',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
findApplicationRegistrationByClientId: application
|
||||
? {
|
||||
__typename: 'PublicApplicationRegistration',
|
||||
...application,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}),
|
||||
),
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () =>
|
||||
HttpResponse.json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: 'Unauthorized',
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
response: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
...graphqlMocks.handlers,
|
||||
];
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/Authorize',
|
||||
component: Authorize,
|
||||
decorators: [PageDecorator],
|
||||
args: {
|
||||
routePath: AppPath.Authorize,
|
||||
routeParams: {},
|
||||
additionalRoutes: [AppPath.NotFound],
|
||||
},
|
||||
parameters: {
|
||||
cookie: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof Authorize>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
searchParams: {
|
||||
client_id: 'default-client-id',
|
||||
redirect_uri: MOCKED_REDIRECT_URL,
|
||||
state: 'mocked-state',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: buildHandlers({
|
||||
id: 'application-id-default',
|
||||
name: 'ChatGPT',
|
||||
logoUrl: null,
|
||||
oAuthScopes: ['api', 'profile'],
|
||||
websiteUrl: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('Connect ChatGPT to your account', undefined, {
|
||||
timeout: 5000,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const WithApiScopeOnly: Story = {
|
||||
args: {
|
||||
searchParams: {
|
||||
client_id: 'api-only-client-id',
|
||||
redirect_uri: MOCKED_REDIRECT_URL,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: buildHandlers({
|
||||
id: 'application-id-api-only',
|
||||
name: 'Internal Tool',
|
||||
logoUrl: null,
|
||||
oAuthScopes: ['api'],
|
||||
websiteUrl: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText(
|
||||
'Connect Internal Tool to your account',
|
||||
undefined,
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLongAppName: Story = {
|
||||
args: {
|
||||
searchParams: {
|
||||
client_id: 'long-name-client-id',
|
||||
redirect_uri: MOCKED_REDIRECT_URL,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: buildHandlers({
|
||||
id: 'application-id-long-name',
|
||||
name: 'Custom Workspace Automation Suite',
|
||||
logoUrl: null,
|
||||
oAuthScopes: ['api', 'profile'],
|
||||
websiteUrl: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText(
|
||||
'Connect Custom Workspace Automation Suite to your account',
|
||||
undefined,
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -38,6 +38,7 @@ export type PageDecoratorArgs = {
|
||||
routePath: string;
|
||||
routeParams: RouteParams;
|
||||
additionalRoutes?: string[];
|
||||
searchParams?: RouteParams;
|
||||
};
|
||||
|
||||
export type RouteParams = {
|
||||
@@ -55,12 +56,18 @@ export const isRouteParams = (obj: any): obj is RouteParams => {
|
||||
export const computeLocation = (
|
||||
routePath: string,
|
||||
routeParams?: RouteParams,
|
||||
searchParams?: RouteParams,
|
||||
) => {
|
||||
const search = searchParams
|
||||
? `?${new URLSearchParams(searchParams).toString()}`
|
||||
: '';
|
||||
|
||||
return {
|
||||
pathname: routePath.replace(
|
||||
/:(\w+)/g,
|
||||
(paramName) => routeParams?.[paramName] ?? '',
|
||||
),
|
||||
search,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -151,13 +158,16 @@ export const PageDecorator: Decorator<{
|
||||
routePath: string;
|
||||
routeParams: RouteParams;
|
||||
additionalRoutes?: string[];
|
||||
searchParams?: RouteParams;
|
||||
}> = (Story, { args }) => {
|
||||
return (
|
||||
<RouterProvider
|
||||
router={createRouter({
|
||||
Story,
|
||||
args,
|
||||
initialEntries: [computeLocation(args.routePath, args.routeParams)],
|
||||
initialEntries: [
|
||||
computeLocation(args.routePath, args.routeParams, args.searchParams),
|
||||
],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user