feat(storybook): progress on many more pages
This commit is contained in:
parent
dbe320b2fc
commit
1e1a638011
|
@ -1,27 +0,0 @@
|
|||
import { KcContextBase, getKcContext } from "keycloakify/lib/getKcContext";
|
||||
import type { DeepPartial } from "keycloakify/lib/tools/DeepPartial";
|
||||
import type { ExtendsKcContextBase } from "keycloakify/lib/getKcContext/getKcContextFromWindow";
|
||||
import type { KcContextExtension } from "keycloak-theme/kcContext";
|
||||
|
||||
|
||||
export const useKcStoryData = (mockData: (
|
||||
{ pageId: KcContextBase['pageId'] | KcContextExtension['pageId'] } & DeepPartial<ExtendsKcContextBase<KcContextExtension>>
|
||||
)) => {
|
||||
const { kcContext } = getKcContext<KcContextExtension>({ mockPageId: mockData.pageId, mockData: [mockData] })
|
||||
return { kcContext: kcContext as NonNullable<typeof kcContext> }
|
||||
}
|
||||
|
||||
export const socialProviders = [
|
||||
{ loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google' },
|
||||
{ loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft' },
|
||||
{ loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook' },
|
||||
{ loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram' },
|
||||
{ loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter' },
|
||||
{ loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn' },
|
||||
{ loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow' },
|
||||
{ loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github' },
|
||||
{ loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab' },
|
||||
{ loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket' },
|
||||
{ loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal' },
|
||||
{ loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift' },
|
||||
]
|
|
@ -1,9 +1,6 @@
|
|||
module.exports = {
|
||||
"stories": [
|
||||
"../src/keycloak-theme/pages/Login.stories.tsx",
|
||||
"../src/keycloak-theme/pages/Register.stories.tsx",
|
||||
"../src/keycloak-theme/pages/MyExtraPage1.stories.tsx",
|
||||
"../src/keycloak-theme/pages/MyExtraPage2.stories.tsx",
|
||||
"../src/keycloak-theme/**/*.stories.tsx",
|
||||
],
|
||||
"addons": [
|
||||
"@storybook/addon-links",
|
||||
|
@ -14,5 +11,6 @@ module.exports = {
|
|||
"framework": "@storybook/react",
|
||||
"core": {
|
||||
"builder": "@storybook/builder-webpack5"
|
||||
}
|
||||
},
|
||||
"staticDirs": ['../public']
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
actions: {argTypesRegex: "^on[A-Z].*"},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
storySort: (a, b) =>
|
||||
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, {numeric: true}),
|
||||
},
|
||||
}
|
41
.storybook/util.tsx
Normal file
41
.storybook/util.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import type {KcContextExtension} from "keycloak-theme/kcContext";
|
||||
import KcApp from "../src/keycloak-theme/KcApp";
|
||||
import {KcContextBase} from "keycloakify";
|
||||
import {getKcContext} from "keycloakify/lib/getKcContext";
|
||||
import {ExtendsKcContextBase} from "keycloakify/src/lib/getKcContext/getKcContextFromWindow";
|
||||
import {DeepPartial} from "keycloakify/src/lib/tools/DeepPartial";
|
||||
|
||||
|
||||
export const socialProviders = [
|
||||
{loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google'},
|
||||
{loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft'},
|
||||
{loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook'},
|
||||
{loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram'},
|
||||
{loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter'},
|
||||
{loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn'},
|
||||
{loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow'},
|
||||
{loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github'},
|
||||
{loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab'},
|
||||
{loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket'},
|
||||
{loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal'},
|
||||
{loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift'},
|
||||
]
|
||||
|
||||
type PageId = (KcContextExtension | KcContextBase)['pageId']
|
||||
export const template = (pageId: PageId) => {
|
||||
type MockData = DeepPartial<ExtendsKcContextBase<KcContextExtension>>;
|
||||
|
||||
const Template = (mockData: MockData) => {
|
||||
const finalMockData = {
|
||||
message: undefined,
|
||||
pageId,
|
||||
...mockData
|
||||
} as MockData
|
||||
if (!("message" in mockData)) mockData["message"] = undefined
|
||||
const {kcContext} = getKcContext<KcContextExtension>({mockPageId: pageId, mockData: [finalMockData]})
|
||||
return <KcApp kcContext={kcContext as NonNullable<typeof kcContext>}/>
|
||||
}
|
||||
|
||||
return (args: MockData) => Object.assign(Template.bind({}), {args})
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
"build": "react-scripts build",
|
||||
"build-keycloak-theme": "yarn build && keycloakify",
|
||||
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook -s public"
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"keycloakify": {
|
||||
"extraPages": [
|
||||
|
|
32
src/keycloak-theme/Template.stories.tsx
Normal file
32
src/keycloak-theme/Template.stories.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from './KcApp';
|
||||
import {template} from '../../.storybook/util'
|
||||
|
||||
const bind = template('my-extra-page-1.ftl');
|
||||
|
||||
export default {
|
||||
title: 'Theme/Template',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
|
||||
export const Default = bind({})
|
||||
export const InFrench = bind({locale: {currentLanguageTag: 'fr'}})
|
||||
export const RealmDisplayNameIsHtml = bind({
|
||||
realm: {
|
||||
displayNameHtml: '<marquee>my realm</marquee>'
|
||||
}
|
||||
})
|
||||
|
||||
export const NoInternationalization = bind({
|
||||
realm: {
|
||||
internationalizationEnabled: false,
|
||||
}
|
||||
})
|
||||
|
||||
export const WithGlobalError = bind({
|
||||
message: {type: "error", summary: "This is an error"}
|
||||
})
|
16
src/keycloak-theme/pages/Error.stories.tsx
Normal file
16
src/keycloak-theme/pages/Error.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
const bind = template('error.ftl');
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Error',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
export const Default = bind({message: {type: 'error', summary: 'Something went wrong'}})
|
34
src/keycloak-theme/pages/Error.tsx
Normal file
34
src/keycloak-theme/pages/Error.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
// copied and adapted from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Error.tsx
|
||||
import React from "react";
|
||||
import type { PageProps } from "keycloakify"
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
|
||||
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const { message, client } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("errorTitle")}
|
||||
formNode={
|
||||
<div id="kc-error-message">
|
||||
<p className="instruction">{message.summary}</p>
|
||||
{client !== undefined && client.baseUrl !== undefined && (
|
||||
<p>
|
||||
<a id="backToApplication" href={client.baseUrl}>
|
||||
{msg("backToApplication")}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
17
src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
Normal file
17
src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Review IDP User Profile',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('idp-review-user-profile.ftl');
|
||||
|
||||
export const Default = bind({})
|
||||
|
47
src/keycloak-theme/pages/IdpReviewUserProfile.tsx
Normal file
47
src/keycloak-theme/pages/IdpReviewUserProfile.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React, { useState } from "react";
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function IdpReviewUserProfile(props: PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||
formNode={
|
||||
<form id="kc-idp-review-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
disabled={!isFomSubmittable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
37
src/keycloak-theme/pages/Info.stories.tsx
Normal file
37
src/keycloak-theme/pages/Info.stories.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Info',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('info.ftl');
|
||||
|
||||
export const Default = bind({
|
||||
messageHeader: 'Yo, get this:',
|
||||
message: {
|
||||
summary: 'You look good today'
|
||||
}
|
||||
})
|
||||
|
||||
export const WithLinkBack = bind({
|
||||
messageHeader: 'Yo, get this:',
|
||||
message: {
|
||||
summary: 'You look good today'
|
||||
},
|
||||
actionUri: undefined
|
||||
})
|
||||
|
||||
export const WithRequiredActions = bind({
|
||||
messageHeader: 'Yo, get this:',
|
||||
message: {
|
||||
summary: 'Before you can carry on, you need to do this: '
|
||||
},
|
||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL"]
|
||||
})
|
49
src/keycloak-theme/pages/Info.tsx
Normal file
49
src/keycloak-theme/pages/Info.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from "react";
|
||||
import {assert} from "keycloakify/lib/tools/assert";
|
||||
import type {PageProps} from "keycloakify";
|
||||
import type {KcContext} from "../kcContext";
|
||||
import type {I18n} from "../i18n";
|
||||
|
||||
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
|
||||
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||
|
||||
const {msgStr, msg} = i18n;
|
||||
|
||||
assert(kcContext.message !== undefined);
|
||||
|
||||
const {messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client} = kcContext;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||
displayMessage={false}
|
||||
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
||||
formNode={
|
||||
<div id="kc-info-message">
|
||||
<p className="instruction">
|
||||
{message.summary}
|
||||
|
||||
{requiredActions !== undefined && (
|
||||
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
|
||||
)}
|
||||
</p>
|
||||
{!skipLink && pageRedirectUri !== undefined ? (
|
||||
<p>
|
||||
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
|
||||
</p>
|
||||
) : actionUri !== undefined ? (
|
||||
<p>
|
||||
<a href={actionUri}>{msg("proceedWithAction")}</a>
|
||||
</p>
|
||||
) : (
|
||||
client.baseUrl !== undefined && (
|
||||
<p>
|
||||
<a href={client.baseUrl}>{msg("backToApplication")}</a>
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,97 +1,27 @@
|
|||
import { ComponentMeta } from '@storybook/react';
|
||||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
|
||||
import { useKcStoryData, socialProviders } from '../../../.storybook/data'
|
||||
import {socialProviders, template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
title: 'Login',
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Login',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const pageId = 'login.ftl'
|
||||
const bind = template('login.ftl');
|
||||
|
||||
export const Default = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const InFrench = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const RealmDisplayNameIsHtml = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined, realm: {
|
||||
displayNameHtml: '<marquee>my realm</marquee>'
|
||||
}
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const NoInternationalization = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined, realm: {
|
||||
internationalizationEnabled: false,
|
||||
}
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const NoPasswordField = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { password: false } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const RegistrationNotAllowed = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { registrationAllowed: false } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const RememberMeNotAllowed = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { rememberMe: false } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const PasswordResetNotAllowed = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { resetPasswordAllowed: false } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const EmailIsUsername = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, realm: { loginWithEmailAllowed: false } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const TryAnotherWay = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, auth: { showTryAnotherWayLink: true } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const PresetUsername = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: 'max.mustermann@mail.com' } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const ReadOnlyPresetUsername = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, login: { username: 'max.mustermann@mail.com' }, usernameEditDisabled: true })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const WithSocialProviders = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined, social: {
|
||||
displayInfo: true,
|
||||
providers: socialProviders
|
||||
}
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const WithError = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const Default = bind({})
|
||||
export const WithoutPasswordField = bind({realm: {password: false}})
|
||||
export const WithoutRegistration = bind({realm: {registrationAllowed: false}})
|
||||
export const WithoutRememberMe = bind({realm: {rememberMe: false}})
|
||||
export const WithoutPasswordReset = bind({realm: {resetPasswordAllowed: false}})
|
||||
export const WithEmailAsUsername = bind({realm: {loginWithEmailAllowed: false}})
|
||||
export const WithPresetUsername = bind({login: {username: 'max.mustermann@mail.com'}})
|
||||
export const WithImmutablePresetUsername = bind({
|
||||
login: {username: 'max.mustermann@mail.com'},
|
||||
usernameEditDisabled: true
|
||||
})
|
||||
export const WithSocialProviders = bind({social: {displayInfo: true, providers: socialProviders}})
|
||||
|
|
27
src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
Normal file
27
src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Configure TOTP',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('login-config-totp.ftl');
|
||||
|
||||
export const Default = bind({})
|
||||
|
||||
export const WithManualSetUp = bind({mode: 'manual'})
|
||||
export const WithError = bind({
|
||||
messagesPerField: {
|
||||
get: (fieldName: string) => fieldName === 'totp' ? 'Invalid TOTP' : undefined,
|
||||
exists: (fieldName: string) => fieldName === 'totp',
|
||||
existsError: (fieldName: string) => fieldName === 'totp',
|
||||
printIfExists: <T, >(fieldName: string, x: T) => fieldName === 'totp' ? x : undefined
|
||||
}
|
||||
})
|
||||
|
186
src/keycloak-theme/pages/LoginConfigTotp.tsx
Normal file
186
src/keycloak-theme/pages/LoginConfigTotp.tsx
Normal file
|
@ -0,0 +1,186 @@
|
|||
import React from "react";
|
||||
import {clsx} from "keycloakify/lib/tools/clsx";
|
||||
import type {PageProps, KcContextBase} from "keycloakify";
|
||||
import type {KcContext} from "../kcContext";
|
||||
import type {I18n} from "../i18n";
|
||||
|
||||
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
|
||||
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||
|
||||
const {url, isAppInitiatedAction, totp, mode, messagesPerField} = kcContext;
|
||||
|
||||
const {msg, msgStr} = i18n;
|
||||
|
||||
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
|
||||
"HmacSHA1": "SHA1",
|
||||
"HmacSHA256": "SHA256",
|
||||
"HmacSHA512": "SHA512"
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||
headerNode={msg("loginTotpTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<ol id="kc-totp-settings">
|
||||
<li>
|
||||
<p>{msg("loginTotpStep1")}</p>
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.policy.supportedApplications.map(app => (
|
||||
<li>{app}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{mode && mode == "manual" ? (
|
||||
<>
|
||||
<li>
|
||||
<p>{msg("loginTotpManualStep2")}</p>
|
||||
<p>
|
||||
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href={totp.qrUrl} id="mode-barcode">
|
||||
{msg("loginTotpScanBarcode")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>{msg("loginTotpManualStep3")}</p>
|
||||
<p>
|
||||
<ul>
|
||||
<li id="kc-totp-type">
|
||||
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
|
||||
</li>
|
||||
<li id="kc-totp-algorithm">
|
||||
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
|
||||
</li>
|
||||
<li id="kc-totp-digits">
|
||||
{msg("loginTotpDigits")}: {totp.policy.digits}
|
||||
</li>
|
||||
{totp.policy.type === "totp" ? (
|
||||
<li id="kc-totp-period">
|
||||
{msg("loginTotpInterval")}: {totp.policy.period}
|
||||
</li>
|
||||
) : (
|
||||
<li id="kc-totp-counter">
|
||||
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</p>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li>
|
||||
<p>{msg("loginTotpStep2")}</p>
|
||||
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
|
||||
alt="Figure: Barcode"/>
|
||||
<br/>
|
||||
<p>
|
||||
<a href={totp.manualUrl} id="mode-manual">
|
||||
{msg("loginTotpUnableToScan")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<p>{msg("loginTotpStep3")}</p>
|
||||
<p>{msg("loginTotpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<form action={url.loginAction} className={clsx(kcProps.kcFormClass)} id="kc-totp-settings-form"
|
||||
method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<label htmlFor="totp" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("authenticatorCode")}
|
||||
</label>{" "}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
autoComplete="off"
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={clsx(kcProps.kcInputErrorMessageClass)}
|
||||
aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret}/>
|
||||
{mode && <input type="hidden" id="mode" value={mode}/>}
|
||||
</div>
|
||||
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<label htmlFor="userLabel" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("loginTotpDeviceName")}
|
||||
</label>{" "}
|
||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="userLabel"
|
||||
name="userLabel"
|
||||
autoComplete="off"
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={clsx(kcProps.kcInputErrorMessageClass)}
|
||||
aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAppInitiatedAction ? (
|
||||
<>
|
||||
<input
|
||||
type="submit"
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonLargeClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
id="cancelTOTPBtn"
|
||||
name="cancel-aia"
|
||||
value="true"
|
||||
>
|
||||
${msg("doCancel")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="submit"
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
17
src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Confirm IDP Link',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('login-idp-link-confirm.ftl');
|
||||
|
||||
export const Default = bind({})
|
||||
|
54
src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx
Normal file
54
src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import {clsx} from "keycloakify/lib/tools/clsx";
|
||||
import type {PageProps} from "keycloakify";
|
||||
import type {KcContext} from "../kcContext";
|
||||
import type {I18n} from "../i18n";
|
||||
|
||||
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
|
||||
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||
|
||||
const {url, idpAlias} = kcContext;
|
||||
|
||||
const {msg} = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||
headerNode={msg("confirmLinkIdpTitle")}
|
||||
formNode={
|
||||
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="submitAction"
|
||||
id="updateProfile"
|
||||
value="updateProfile"
|
||||
>
|
||||
{msg("confirmLinkIdpReviewProfile")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="submitAction"
|
||||
id="linkAccount"
|
||||
value="linkAccount"
|
||||
>
|
||||
{msg("confirmLinkIdpContinue", idpAlias)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
17
src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Confirm IDP Email',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('login-idp-link-email.ftl');
|
||||
|
||||
export const Default = bind({})
|
||||
|
32
src/keycloak-theme/pages/LoginIdpLinkEmail.tsx
Normal file
32
src/keycloak-theme/pages/LoginIdpLinkEmail.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import type {PageProps} from "keycloakify";
|
||||
import type {KcContext} from "../kcContext";
|
||||
import type {I18n} from "../i18n";
|
||||
|
||||
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
|
||||
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||
|
||||
const {url, realm, brokerContext, idpAlias} = kcContext;
|
||||
|
||||
const {msg} = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||
headerNode={msg("emailLinkIdpTitle", idpAlias)}
|
||||
formNode={
|
||||
<>
|
||||
<p id="instruction1" className="instruction">
|
||||
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||
</p>
|
||||
<p id="instruction2" className="instruction">
|
||||
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
|
||||
</p>
|
||||
<p id="instruction3" className="instruction">
|
||||
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
16
src/keycloak-theme/pages/LoginOtp.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginOtp.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {socialProviders, template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Login OTP',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('login-otp.ftl');
|
||||
|
||||
export const Default = bind({})
|
114
src/keycloak-theme/pages/LoginOtp.tsx
Normal file
114
src/keycloak-theme/pages/LoginOtp.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { headInsert } from "keycloakify/lib/tools/headInsert";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const { otpLogin, url } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
useEffect(() => {
|
||||
let isCleanedUp = false;
|
||||
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
|
||||
}).then(() => {
|
||||
if (isCleanedUp) return;
|
||||
|
||||
evaluateInlineScript();
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCleanedUp = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("doLogIn")}
|
||||
formNode={
|
||||
<form id="kc-otp-login-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
{otpLogin.userOtpCredentials.length > 1 && (
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
{otpLogin.userOtpCredentials.map(otpCredential => (
|
||||
<div key={otpCredential.id} className={clsx(kcProps.kcSelectOTPListClass)}>
|
||||
<input type="hidden" value="${otpCredential.id}" />
|
||||
<div className={clsx(kcProps.kcSelectOTPListItemClass)}>
|
||||
<span className={clsx(kcProps.kcAuthenticatorOtpCircleClass)} />
|
||||
<h2 className={clsx(kcProps.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="otp" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("loginOtpOneTime")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input id="otp" name="otp" autoComplete="off" type="text" className={clsx(kcProps.kcInputClass)} autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="login"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
value={msgStr("doLogIn")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
declare const $: any;
|
||||
|
||||
function evaluateInlineScript() {
|
||||
$(document).ready(function () {
|
||||
// Card Single Select
|
||||
$(".card-pf-view-single-select").click(function (this: any) {
|
||||
if ($(this).hasClass("active")) {
|
||||
$(this).removeClass("active");
|
||||
$(this).children().removeAttr("name");
|
||||
} else {
|
||||
$(".card-pf-view-single-select").removeClass("active");
|
||||
$(".card-pf-view-single-select").children().removeAttr("name");
|
||||
$(this).addClass("active");
|
||||
$(this).children().attr("name", "selectedCredentialId");
|
||||
}
|
||||
});
|
||||
|
||||
var defaultCred = $(".card-pf-view-single-select")[0];
|
||||
if (defaultCred) {
|
||||
defaultCred.click();
|
||||
}
|
||||
});
|
||||
}
|
16
src/keycloak-theme/pages/LoginPageExpired.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginPageExpired.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {socialProviders, template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Login/Login Page Expired',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const bind = template('login-page-expired.ftl');
|
||||
|
||||
export const Default = bind({})
|
36
src/keycloak-theme/pages/LoginPageExpired.tsx
Normal file
36
src/keycloak-theme/pages/LoginPageExpired.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("pageExpiredTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<p id="instruction1" className="instruction">
|
||||
{msg("pageExpiredMsg1")}
|
||||
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||
{msg("doClickHere")}
|
||||
</a>{" "}
|
||||
.<br />
|
||||
{msg("pageExpiredMsg2")}{" "}
|
||||
<a id="loginContinueLink" href={url.loginAction}>
|
||||
{msg("doClickHere")}
|
||||
</a>{" "}
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,24 +1,16 @@
|
|||
import { ComponentMeta } from '@storybook/react';
|
||||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
|
||||
import { useKcStoryData } from '../../../.storybook/data'
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
title: 'My Extra Page 1',
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/My Extra Page 1',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const pageId = 'my-extra-page-1.ftl'
|
||||
const bind = template('my-extra-page-1.ftl')
|
||||
|
||||
export const Default = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const InFrench = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const Default = bind({})
|
||||
|
|
|
@ -1,29 +1,18 @@
|
|||
import { ComponentMeta } from '@storybook/react';
|
||||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
|
||||
import { useKcStoryData } from '../../../.storybook/data'
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
title: 'My Extra Page 2',
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/My Extra Page 2',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const pageId = 'my-extra-page-2.ftl'
|
||||
const bind = template('my-extra-page-2.ftl')
|
||||
|
||||
export const Default = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const Default = bind({})
|
||||
|
||||
export const InFrench = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, locale: { currentLanguageTag: 'fr' } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const WithCustomValue = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, someCustomValue: 'Foo Bar Baz' })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const WithCustomValue = bind({someCustomValue: 'Foo Bar Baz'})
|
||||
|
|
|
@ -1,60 +1,48 @@
|
|||
import { ComponentMeta } from '@storybook/react';
|
||||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
|
||||
import { useKcStoryData, socialProviders } from '../../../.storybook/data'
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
export default {
|
||||
title: 'Register',
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Register',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
const pageId = 'register.ftl'
|
||||
const bind = template('register.ftl')
|
||||
|
||||
export const Default = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const Default = bind({})
|
||||
|
||||
export const InFrench = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: undefined, locale: { currentLanguageTag: 'fr' } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const WithFieldError = bind({
|
||||
register: {
|
||||
formData: {
|
||||
email: 'max.mustermann@mail.com'
|
||||
}
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (fieldName: string) => fieldName === "email",
|
||||
exists: (fieldName: string) => fieldName === "email",
|
||||
get: (fieldName: string) => fieldName === "email" ? "I don't like your email address" : undefined,
|
||||
printIfExists: <T, >(fieldName: string, x: T) => fieldName === "email" ? x : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
export const WithError = () => {
|
||||
const { kcContext } = useKcStoryData({ pageId, message: { type: "error", summary: "This is an error" } })
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const WithEmailAsUsername = bind({
|
||||
realm: {registrationEmailAsUsername: true}
|
||||
})
|
||||
|
||||
export const EmailIsUsername = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined,
|
||||
realm: { registrationEmailAsUsername: true }
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
export const WithoutPassword = bind({
|
||||
passwordRequired: false
|
||||
})
|
||||
|
||||
export const NoPassword = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined, passwordRequired: false
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
|
||||
export const WithRecaptcha = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined,
|
||||
export const WithRecaptcha = bind({
|
||||
recaptchaRequired: true,
|
||||
recaptchaSiteKey: 'foobar'
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
}
|
||||
})
|
||||
|
||||
export const WithPresets = () => {
|
||||
const { kcContext } = useKcStoryData({
|
||||
pageId, message: undefined,
|
||||
export const WithPresets = bind({
|
||||
register: {
|
||||
formData: {
|
||||
firstName: 'Max',
|
||||
|
@ -63,7 +51,4 @@ export const WithPresets = () => {
|
|||
username: 'max.mustermann'
|
||||
}
|
||||
}
|
||||
})
|
||||
return <KcApp kcContext={kcContext} />
|
||||
|
||||
}
|
||||
})
|
|
@ -54,6 +54,7 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
|
|||
name="lastName"
|
||||
defaultValue={register.formData.lastName ?? ""}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
79
src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
Normal file
79
src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
const bind = template('register-user-profile.ftl')
|
||||
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Register User Profile',
|
||||
component: KcApp,
|
||||
parameters: {layout: 'fullscreen'},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
export const Default = bind({})
|
||||
|
||||
export const WithFieldError = bind({
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
name: "email",
|
||||
value: "max.mustermann@mail.com",
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
export const WithPresets = bind({
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
name: "username",
|
||||
value: "max.mustermann"
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
value: "max.mustermann@gmail.com",
|
||||
},
|
||||
{
|
||||
name: "firstName",
|
||||
required: false,
|
||||
value: "Max"
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
required: false,
|
||||
value: "Mustermann"
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
export const WithImmutablePresets = bind({
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
name: "username",
|
||||
value: "max.mustermann",
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
value: "max.mustermann@gmail.com",
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
name: "firstName",
|
||||
required: true,
|
||||
value: "Max",
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
required: true,
|
||||
value: "Mustermann",
|
||||
readOnly: true,
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
15
src/keycloak-theme/pages/Terms.stories.tsx
Normal file
15
src/keycloak-theme/pages/Terms.stories.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {ComponentMeta} from '@storybook/react';
|
||||
import KcApp from '../KcApp';
|
||||
import {template} from '../../../.storybook/util'
|
||||
|
||||
const bind = template('terms.ftl');
|
||||
export default {
|
||||
kind: 'Page',
|
||||
title: 'Theme/Pages/Terms',
|
||||
component: KcApp,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof KcApp>;
|
||||
|
||||
export const Default = bind({})
|
|
@ -1,43 +1,49 @@
|
|||
/**
|
||||
* NOTE: Yo do not need to do all this to put your own Terms and conditions
|
||||
* NOTE: You do not need to do all this to put your own Terms and conditions
|
||||
* this is if you want component level customization.
|
||||
* If the default works for you you can just use the useDownloadTerms hook
|
||||
* If the default works for you, you can just use the useDownloadTerms hook
|
||||
* in the KcApp.tsx
|
||||
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
|
||||
*/
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import { Markdown } from "keycloakify/lib/tools/Markdown";
|
||||
import { evtTermMarkdown, useDownloadTerms } from "keycloakify/lib/pages/Terms";
|
||||
import {clsx} from "keycloakify/lib/tools/clsx";
|
||||
import {useRerenderOnStateChange} from "evt/hooks";
|
||||
import {Markdown} from "keycloakify/lib/tools/Markdown";
|
||||
import {evtTermMarkdown, useDownloadTerms} from "keycloakify/lib/pages/Terms";
|
||||
import tos_en_url from "../assets/tos_en.md";
|
||||
import tos_fr_url from "../assets/tos_fr.md";
|
||||
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import type {PageProps} from "keycloakify/lib/KcProps";
|
||||
import type {KcContext} from "../kcContext";
|
||||
import type {I18n} from "../i18n";
|
||||
|
||||
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl"; }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
const {msg, msgStr} = i18n;
|
||||
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
"downloadTermMarkdown": async ({ currentLanguageTag }) => {
|
||||
"downloadTermMarkdown": async ({currentLanguageTag}) => {
|
||||
|
||||
const markdownString = await fetch((() => {
|
||||
const resource = (() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return tos_fr_url;
|
||||
default: return tos_en_url;
|
||||
case "fr":
|
||||
return tos_fr_url;
|
||||
default:
|
||||
return tos_en_url;
|
||||
}
|
||||
})()).then(response => response.text());
|
||||
})();
|
||||
|
||||
return markdownString;
|
||||
// webpack5 (used via storybook) loads markdown as string, not url
|
||||
if (resource.includes("\n")) return resource
|
||||
|
||||
const response = await fetch(resource);
|
||||
return response.text();
|
||||
},
|
||||
});
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
const { url } = kcContext;
|
||||
const {url} = kcContext;
|
||||
|
||||
if (evtTermMarkdown.state === undefined) {
|
||||
return null;
|
||||
|
@ -45,12 +51,13 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
|
|||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||
displayMessage={false}
|
||||
headerNode={msg("termsTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state &&
|
||||
<Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={clsx(
|
||||
|
@ -73,7 +80,7 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
|
|||
value={msgStr("doDecline")}
|
||||
/>
|
||||
</form>
|
||||
<div className="clearfix" />
|
||||
<div className="clearfix"/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue