From c48eee0ebf76003393019e9cfb05956c3469a9ea Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 8 Jun 2023 13:43:13 -0700 Subject: [PATCH] web: add visualizing and testing for the FieldRenderers --- .../stages/prompt/FieldRenderers.stories.ts | 108 ++++++++++++++++++ web/src/flow/stages/prompt/FieldRenderers.ts | 19 +++ web/src/flow/stages/prompt/PromptStage.ts | 75 ++++-------- web/src/flow/stages/prompt/helpers.ts | 37 ++++++ 4 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 web/src/flow/stages/prompt/FieldRenderers.stories.ts create mode 100644 web/src/flow/stages/prompt/helpers.ts diff --git a/web/src/flow/stages/prompt/FieldRenderers.stories.ts b/web/src/flow/stages/prompt/FieldRenderers.stories.ts new file mode 100644 index 000000000..4ed541904 --- /dev/null +++ b/web/src/flow/stages/prompt/FieldRenderers.stories.ts @@ -0,0 +1,108 @@ +import { TemplateResult, html } from "lit"; + +import "@patternfly/patternfly/components/Alert/alert.css"; +import "@patternfly/patternfly/components/Button/button.css"; +import "@patternfly/patternfly/components/Check/check.css"; +import "@patternfly/patternfly/components/Form/form.css"; +import "@patternfly/patternfly/components/FormControl/form-control.css"; +import "@patternfly/patternfly/components/Login/login.css"; +import "@patternfly/patternfly/components/Title/title.css"; +import "@patternfly/patternfly/patternfly-base.css"; + +import { PromptTypeEnum } from "@goauthentik/api"; +import type { StagePrompt } from "@goauthentik/api"; + +import promptRenderers from "./FieldRenderers"; +import { renderContinue, renderPromptHelpText, renderPromptInner } from "./helpers"; + +// Storybook stories are meant to show not just that the objects work, but to document good +// practices around using them. Because of their uniform signature, the renderers can easily +// be encapsulated into containers that show them at their most functional, even without +// building Shadow DOMs with which to do it. This is 100% Light DOM work, and they still +// work well. + +const baseRenderer = (prompt: TemplateResult) => + html`
+ + ${prompt} +
`; + +function renderer(kind: PromptTypeEnum, prompt: Partial) { + const renderer = promptRenderers.get(kind); + if (!renderer) { + throw new Error(`A renderer of type ${kind} does not exist.`); + } + return baseRenderer(html`${renderer(prompt as StagePrompt)}`); +} + +const textPrompt = { + fieldKey: "test_text_field", + placeholder: "This is the placeholder", + required: false, + initialValue: "initial value", +}; + +export const Text = () => renderer(PromptTypeEnum.Text, textPrompt); +export const TextArea = () => renderer(PromptTypeEnum.TextArea, textPrompt); +export const TextReadOnly = () => renderer(PromptTypeEnum.TextReadOnly, textPrompt); +export const TextAreaReadOnly = () => renderer(PromptTypeEnum.TextAreaReadOnly, textPrompt); +export const Username = () => renderer(PromptTypeEnum.Username, textPrompt); +export const Password = () => renderer(PromptTypeEnum.Password, textPrompt); + +const emailPrompt = { ...textPrompt, initialValue: "example@example.fun" }; +export const Email = () => renderer(PromptTypeEnum.Email, emailPrompt); + +const numberPrompt = { ...textPrompt, initialValue: "10" }; +export const Number = () => renderer(PromptTypeEnum.Number, numberPrompt); + +const datePrompt = { ...textPrompt, initialValue: "2018-06-12T19:30" }; +export const Date = () => renderer(PromptTypeEnum.Date, datePrompt); +export const DateTime = () => renderer(PromptTypeEnum.DateTime, datePrompt); + +const separatorPrompt = { placeholder: "😊" }; +export const Separator = () => renderer(PromptTypeEnum.Separator, separatorPrompt); + +const staticPrompt = { initialValue: "😊" }; +export const Static = () => renderer(PromptTypeEnum.Static, staticPrompt); + +const choicePrompt = { + fieldKey: "test_text_field", + placeholder: "This is the placeholder", + required: false, + initialValue: "first", + choices: ["first", "second", "third"], +}; + +export const Dropdown = () => renderer(PromptTypeEnum.Dropdown, choicePrompt); +export const RadioButtonGroup = () => renderer(PromptTypeEnum.RadioButtonGroup, choicePrompt); + +const checkPrompt = { ...textPrompt, label: "Favorite Subtext?", subText: "(Xena & Gabrielle)" }; +export const Checkbox = () => renderer(PromptTypeEnum.Checkbox, checkPrompt); + +const localePrompt = { ...textPrompt, initialValue: "en" }; +export const Locale = () => renderer(PromptTypeEnum.AkLocale, localePrompt); + +export const PromptFailure = () => + baseRenderer(renderPromptInner({ type: null } as unknown as StagePrompt)); + +export const HelpText = () => + baseRenderer(renderPromptHelpText({ subText: "There is no subtext here." } as StagePrompt)); + +export const Continue = () => baseRenderer(renderContinue()); + +export default { + title: "Flow Components/Field Renderers", +}; diff --git a/web/src/flow/stages/prompt/FieldRenderers.ts b/web/src/flow/stages/prompt/FieldRenderers.ts index 535605195..8ce18669d 100644 --- a/web/src/flow/stages/prompt/FieldRenderers.ts +++ b/web/src/flow/stages/prompt/FieldRenderers.ts @@ -207,6 +207,24 @@ export function renderRadioButtonGroup(prompt: StagePrompt) { })}`; } +export function renderCheckbox(prompt: StagePrompt) { + return html`
+ + + ${prompt.required + ? html`

${msg("Required.")}

` + : html``} +

${unsafeHTML(prompt.subText)}

+
`; +} + export function renderAkLocale(prompt: StagePrompt) { // TODO: External reference. const inDebug = rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanDebug); @@ -247,6 +265,7 @@ export const promptRenderers = new Map([ [PromptTypeEnum.Static, renderStatic], [PromptTypeEnum.Dropdown, renderDropdown], [PromptTypeEnum.RadioButtonGroup, renderRadioButtonGroup], + [PromptTypeEnum.Checkbox, renderCheckbox], [PromptTypeEnum.AkLocale, renderAkLocale], ]); diff --git a/web/src/flow/stages/prompt/PromptStage.ts b/web/src/flow/stages/prompt/PromptStage.ts index 4d422e4ce..704be1b5c 100644 --- a/web/src/flow/stages/prompt/PromptStage.ts +++ b/web/src/flow/stages/prompt/PromptStage.ts @@ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base"; import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement } from "lit/decorators.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; @@ -24,7 +23,13 @@ import { StagePrompt, } from "@goauthentik/api"; -import promptRenderers from "./FieldRenderers"; +import { renderCheckbox } from "./FieldRenderers"; +import { + renderContinue, + renderPromptHelpText, + renderPromptInner, + shouldRenderInWrapper, +} from "./helpers"; @customElement("ak-stage-prompt") export class PromptStage extends BaseStage { @@ -48,70 +53,35 @@ export class PromptStage extends BaseStageinvalid type '${prompt.type}'

`; - } - return renderer(prompt); - } + /* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */ - renderPromptHelpText(prompt: StagePrompt): TemplateResult { - if (prompt.subText === "") { - return html``; - } - return html`

${unsafeHTML(prompt.subText)}

`; + renderPromptInner(prompt: StagePrompt) { + return renderPromptInner(prompt); } - - shouldRenderInWrapper(prompt: StagePrompt): boolean { - // Special types that aren't rendered in a wrapper - const specialTypes = [ - PromptTypeEnum.Static, - PromptTypeEnum.Hidden, - PromptTypeEnum.Separator, - ]; - const special = specialTypes.find((s) => s === prompt.type); - return !special; + renderPromptHelpText(prompt: StagePrompt) { + return renderPromptHelpText(prompt); + } + shouldRenderInWrapper(prompt: StagePrompt) { + return shouldRenderInWrapper(prompt); } renderField(prompt: StagePrompt): TemplateResult { - // Checkbox is rendered differently + // Checkbox has a slightly different layout, so it must be intercepted early. if (prompt.type === PromptTypeEnum.Checkbox) { - return html`
- - - ${prompt.required - ? html`

${msg("Required.")}

` - : html``} -

${unsafeHTML(prompt.subText)}

-
`; + return renderCheckbox(prompt); } - if (this.shouldRenderInWrapper(prompt)) { + + if (shouldRenderInWrapper(prompt)) { return html` - ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)} + ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)} `; } - return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`; - } - - renderContinue(): TemplateResult { - return html`
- -
`; + return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`; } render(): TemplateResult { @@ -119,6 +89,7 @@ export class PromptStage extends BaseStage `; } + return html` @@ -137,7 +108,7 @@ export class PromptStage extends BaseStage