web: bad default in select (#8258)
* web: fix event propogation in search-select wrappers Two different patches, an older one that extracted long search blocks that were cut-and-pasted into a standalone component, and a newer one that fixed displaying placeholder values properly, conflicted and broke a relationship that allowed for the values to be propagated through those standalone components correctly. This restores the event handling and updates the listener set-ups with more idiomatic hooks into Lit's event system. * Updated search-select to properly render with Storybook, and provided a foundation for testing the Search-Select component with Storybook. * Accidentally deleted this line while making Sonar accept my test data. * Fixing a small issue that's bugged me for awhile: there's no reason to manually duplicate what code can duplicate. * Provided a storybook for testing out the flow search. Discovered along the way that I'd mis-used a prop-drilling technique which caused the currentFlow to be "undefined" when pass forward, giving rise to Marc's bug. I *think* this shakes out the last of the bugs. Events are passed up correctly and the initial value is recorded correctly. * Added comments and prettier had opinions. * Restoring old variable names; they didn't have to change after all. * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -46,8 +46,8 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
currentFlow: string | undefined;
|
||||
@property({ type: String })
|
||||
currentFlow?: string | undefined;
|
||||
|
||||
/**
|
||||
* If true, it is not valid to leave the flow blank.
|
||||
|
136
web/src/admin/common/ak-flow-search/ak-flow-search.stories.ts
Normal file
136
web/src/admin/common/ak-flow-search/ak-flow-search.stories.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { AkFlowSearch } from "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import { Flow, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
|
||||
const mockData = {
|
||||
pagination: {
|
||||
next: 0,
|
||||
previous: 0,
|
||||
count: 2,
|
||||
current: 1,
|
||||
total_pages: 1,
|
||||
start_index: 1,
|
||||
end_index: 2,
|
||||
},
|
||||
results: [
|
||||
{
|
||||
pk: "41468774-bef6-4ffb-b675-332d0d8c5d25",
|
||||
policybindingmodel_ptr_id: "0fb5b872-2734-44bd-ac7e-f23051481a83",
|
||||
name: "Authorize Application",
|
||||
slug: "default-provider-authorization-explicit-consent",
|
||||
title: "Redirecting to %(app)s",
|
||||
designation: "authorization",
|
||||
background: "/static/dist/assets/images/flow_background.jpg",
|
||||
stages: ["8adcdc74-0d3d-48a8-b628-38e3da4081e5"],
|
||||
policies: [],
|
||||
cache_count: 0,
|
||||
policy_engine_mode: "any",
|
||||
compatibility_mode: false,
|
||||
export_url:
|
||||
"/api/v3/flows/instances/default-provider-authorization-explicit-consent/export/",
|
||||
layout: "stacked",
|
||||
denied_action: "message_continue",
|
||||
authentication: "require_authenticated",
|
||||
},
|
||||
{
|
||||
pk: "89f57fd8-fd1e-42be-a5fd-abc13b19529b",
|
||||
policybindingmodel_ptr_id: "e8526408-c6ee-46e1-bbfe-a1d37c2c02c8",
|
||||
name: "Authorize Application",
|
||||
slug: "default-provider-authorization-implicit-consent",
|
||||
title: "Redirecting to %(app)s",
|
||||
designation: "authorization",
|
||||
background: "/static/dist/assets/images/flow_background.jpg",
|
||||
stages: [],
|
||||
policies: [],
|
||||
cache_count: 0,
|
||||
policy_engine_mode: "any",
|
||||
compatibility_mode: false,
|
||||
export_url:
|
||||
"/api/v3/flows/instances/default-provider-authorization-implicit-consent/export/",
|
||||
layout: "stacked",
|
||||
denied_action: "message_continue",
|
||||
authentication: "require_authenticated",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const metadata: Meta<AkFlowSearch<Flow>> = {
|
||||
title: "Elements / Select Search / Flow",
|
||||
component: "ak-flow-search",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A Select Search for Authentication Flows",
|
||||
},
|
||||
},
|
||||
mockData: [
|
||||
{
|
||||
url: `${window.location.origin}/api/v3/flows/instances/?designation=authorization&ordering=slug`,
|
||||
method: "GET",
|
||||
status: 200,
|
||||
response: () => mockData,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) => {
|
||||
return html` <div style="background: #fff; padding: 1.0rem;">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
${testItem}
|
||||
<ul id="message-pad" style="margin-top: 1em; min-height: 5em;"></ul>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.target.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const Default = () =>
|
||||
container(
|
||||
html` <ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
@input=${displayChange}
|
||||
></ak-flow-search
|
||||
></ak-form-element-horizontal>`,
|
||||
);
|
||||
|
||||
export const WithInitialValue = () =>
|
||||
container(
|
||||
html` <ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
currentFlow="89f57fd8-fd1e-42be-a5fd-abc13b19529b"
|
||||
@input=${displayChange}
|
||||
></ak-flow-search
|
||||
></ak-form-element-horizontal>`,
|
||||
);
|
145
web/src/components/stories/ak-search-select.stories.ts
Normal file
145
web/src/components/stories/ak-search-select.stories.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { groupBy } from "@goauthentik/app/common/utils";
|
||||
import { convertToSlug as slugify } from "@goauthentik/common/utils.js";
|
||||
import "@goauthentik/elements/forms/SearchSelect/ak-search-select";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect/ak-search-select";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
type RawSample = [string, string[]];
|
||||
type Sample = { name: string; pk: string; season: string[] };
|
||||
|
||||
// prettier-ignore
|
||||
const groupedSamples: RawSample[] = [
|
||||
["Spring", [
|
||||
"Apples", "Apricots", "Asparagus", "Avocados", "Bananas", "Broccoli",
|
||||
"Cabbage", "Carrots", "Celery", "Collard Greens", "Garlic", "Herbs", "Kale", "Kiwifruit", "Lemons",
|
||||
"Lettuce", "Limes", "Mushrooms", "Onions", "Peas", "Pineapples", "Radishes", "Rhubarb", "Spinach",
|
||||
"Strawberries", "Swiss Chard", "Turnips"]],
|
||||
["Summer", [
|
||||
"Apples", "Apricots", "Avocados", "Bananas", "Beets", "Bell Peppers", "Blackberries", "Blueberries",
|
||||
"Cantaloupe", "Carrots", "Celery", "Cherries", "Corn", "Cucumbers", "Eggplant", "Garlic",
|
||||
"Green Beans", "Herbs", "Honeydew Melon", "Lemons", "Lima Beans", "Limes", "Mangos", "Okra", "Peaches",
|
||||
"Plums", "Raspberries", "Strawberries", "Summer Squash", "Tomatillos", "Tomatoes", "Watermelon",
|
||||
"Zucchini"]],
|
||||
["Fall", [
|
||||
"Apples", "Bananas", "Beets", "Bell Peppers", "Broccoli", "Brussels Sprouts", "Cabbage", "Carrots",
|
||||
"Cauliflower", "Celery", "Collard Greens", "Cranberries", "Garlic", "Ginger", "Grapes", "Green Beans",
|
||||
"Herbs", "Kale", "Kiwifruit", "Lemons", "Lettuce", "Limes", "Mangos", "Mushrooms", "Onions",
|
||||
"Parsnips", "Pears", "Peas", "Pineapples", "Potatoes", "Pumpkin", "Radishes", "Raspberries",
|
||||
"Rutabagas", "Spinach", "Sweet Potatoes", "Swiss Chard", "Turnips", "Winter Squash"]],
|
||||
["Winter", [
|
||||
"Apples", "Avocados", "Bananas", "Beets", "Brussels Sprouts", "Cabbage", "Carrots", "Celery",
|
||||
"Collard Greens", "Grapefruit", "Herbs", "Kale", "Kiwifruit", "Leeks", "Lemons", "Limes", "Onions",
|
||||
"Oranges", "Parsnips", "Pears", "Pineapples", "Potatoes", "Pumpkin", "Rutabagas",
|
||||
"Sweet Potatoes", "Swiss Chard", "Turnips", "Winter Squash"]]
|
||||
];
|
||||
|
||||
// WAAAAY too many lines to turn the arrays above into a Sample of
|
||||
// { name: "Apricots", pk: "apple", season: ["Spring", "Summer"] }
|
||||
// but it does the job.
|
||||
|
||||
const samples = Array.from(
|
||||
groupedSamples
|
||||
.reduce((acc, sample) => {
|
||||
sample[1].forEach((item) => {
|
||||
const update = (thing: Sample) => ({
|
||||
...thing,
|
||||
season: [...thing.season, sample[0]],
|
||||
});
|
||||
acc.set(
|
||||
item,
|
||||
update(acc.get(item) || { name: item, pk: slugify(item), season: [] }),
|
||||
);
|
||||
return acc;
|
||||
}, acc);
|
||||
return acc;
|
||||
}, new Map<string, Sample>())
|
||||
.values(),
|
||||
);
|
||||
samples.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
|
||||
// All we need is a promise to return our dataset. It doesn't have to be a class-based method a'la
|
||||
// the authentik API.
|
||||
|
||||
const getSamples = (query = "") =>
|
||||
Promise.resolve(
|
||||
samples.filter((s) =>
|
||||
query !== "" ? s.name.toLowerCase().includes(query.toLowerCase()) : true,
|
||||
),
|
||||
);
|
||||
|
||||
const metadata: Meta<SearchSelect<Sample>> = {
|
||||
title: "Elements / Search Select ",
|
||||
component: "ak-search-select",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "An implementation of the Patternfly search select pattern",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #fff; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="message-pad" style="margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.detail.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return container(
|
||||
html`<ak-search-select
|
||||
.fetchObjects=${getSamples}
|
||||
.renderElement=${(sample: Sample) => sample.name}
|
||||
.value=${(sample: Sample) => sample.pk}
|
||||
@ak-change=${displayChange}
|
||||
></ak-search-select>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const Grouped = () => {
|
||||
return container(
|
||||
html`<ak-search-select
|
||||
.fetchObjects=${getSamples}
|
||||
.renderElement=${(sample: Sample) => sample.name}
|
||||
.value=${(sample: Sample) => sample.pk}
|
||||
.groupBy=${(samples: Sample[]) =>
|
||||
groupBy(samples, (sample: Sample) => sample.season[0] ?? "")}
|
||||
@ak-change=${displayChange}
|
||||
></ak-search-select>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const Selected = () => {
|
||||
return container(
|
||||
html`<ak-search-select
|
||||
.fetchObjects=${getSamples}
|
||||
.renderElement=${(sample: Sample) => sample.name}
|
||||
.value=${(sample: Sample) => sample.pk}
|
||||
.selected=${(sample: Sample) => sample.pk === "herbs"}
|
||||
@ak-change=${displayChange}
|
||||
></ak-search-select>`,
|
||||
);
|
||||
};
|
@ -3,6 +3,7 @@ import { PreventFormSubmit } from "@goauthentik/app/elements/forms/helpers";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@ -105,12 +106,17 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
@state()
|
||||
error?: APIErrorTypes;
|
||||
|
||||
static styles = [PFBase, PFForm, PFFormControl, PFSelect];
|
||||
static get styles() {
|
||||
return [PFBase, PFForm, PFFormControl, PFSelect];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!document.adoptedStyleSheets.includes(PFDropdown)) {
|
||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFDropdown];
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
ensureCSSStyleSheet(PFDropdown),
|
||||
];
|
||||
}
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.observer = new IntersectionObserver(() => {
|
||||
@ -150,6 +156,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
objects.forEach((obj) => {
|
||||
if (this.selected && this.selected(obj, objects || [])) {
|
||||
this.selectedObject = obj;
|
||||
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
|
||||
}
|
||||
});
|
||||
this.objects = objects;
|
||||
|
Reference in New Issue
Block a user