mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
Compare commits
21 Commits
web/add-ht
...
web/testin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0256a0535 | ||
|
|
142a985914 | ||
|
|
a8531d498a | ||
|
|
f8cb4e880b | ||
|
|
3ced637db3 | ||
|
|
409934196c | ||
|
|
62c882cb0e | ||
|
|
033b55ba51 | ||
|
|
e5e14d3b5a | ||
|
|
9475c1b0cf | ||
|
|
6875efcfdd | ||
|
|
5cf4172a6f | ||
|
|
dbfa5f2fd1 | ||
|
|
e44341d5e0 | ||
|
|
f77ee77f7e | ||
|
|
d4b39b30cb | ||
|
|
b0507d2063 | ||
|
|
d0a459076b | ||
|
|
e20eaac56e | ||
|
|
085ab3c2dd | ||
|
|
c0063c1749 |
@@ -45,6 +45,7 @@ class TestFlowInspector(APITestCase):
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{
|
||||
"allow_show_password": False,
|
||||
"component": "ak-stage-identification",
|
||||
"flow_info": {
|
||||
"background": flow.background_url,
|
||||
|
||||
@@ -38,10 +38,11 @@ class IdentificationStage(Stage):
|
||||
help_text=_(
|
||||
(
|
||||
"When set, shows a password field, instead of showing the "
|
||||
"password field as seaprate step."
|
||||
"password field as separate step."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
case_insensitive_matching = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("When enabled, user fields are matched regardless of their casing."),
|
||||
|
||||
@@ -64,6 +64,7 @@ class IdentificationChallenge(Challenge):
|
||||
|
||||
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
|
||||
password_fields = BooleanField()
|
||||
allow_show_password = BooleanField(default=False)
|
||||
application_pre = CharField(required=False)
|
||||
flow_designation = ChoiceField(FlowDesignation.choices)
|
||||
|
||||
@@ -197,6 +198,8 @@ class IdentificationStageView(ChallengeStageView):
|
||||
"primary_action": self.get_primary_action(),
|
||||
"user_fields": current_stage.user_fields,
|
||||
"password_fields": bool(current_stage.password_stage),
|
||||
"allow_show_password": bool(current_stage.password_stage)
|
||||
and current_stage.password_stage.allow_show_password,
|
||||
"show_source_labels": current_stage.show_source_labels,
|
||||
"flow_designation": self.executor.flow.designation,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class PasswordStageSerializer(StageSerializer):
|
||||
"backends",
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
"allow_show_password",
|
||||
]
|
||||
|
||||
|
||||
@@ -28,6 +29,7 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"name",
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
"allow_show_password",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.6 on 2024-07-02 18:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_password", "0008_replace_inbuilt"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="passwordstage",
|
||||
name="allow_show_password",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, provides a 'show password' button with the password input field.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -43,6 +43,12 @@ class PasswordStage(ConfigurableStage, Stage):
|
||||
"To lock the user out, use a reputation policy and a user_write stage."
|
||||
),
|
||||
)
|
||||
allow_show_password = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"When enabled, provides a 'show password' button with the password input field."
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
from sentry_sdk.hub import Hub
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@@ -76,6 +76,8 @@ class PasswordChallenge(WithUserInfoChallenge):
|
||||
|
||||
component = CharField(default="ak-stage-password")
|
||||
|
||||
allow_show_password = BooleanField(default=False)
|
||||
|
||||
|
||||
class PasswordChallengeResponse(ChallengeResponse):
|
||||
"""Password challenge response"""
|
||||
@@ -134,7 +136,11 @@ class PasswordStageView(ChallengeStageView):
|
||||
response_class = PasswordChallengeResponse
|
||||
|
||||
def get_challenge(self) -> Challenge:
|
||||
challenge = PasswordChallenge(data={})
|
||||
challenge = PasswordChallenge(
|
||||
data={
|
||||
"allow_show_password": self.executor.current_stage.allow_show_password,
|
||||
}
|
||||
)
|
||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
||||
if recovery_flow.exists():
|
||||
recover_url = reverse(
|
||||
|
||||
@@ -6905,7 +6905,7 @@
|
||||
"password_stage": {
|
||||
"type": "integer",
|
||||
"title": "Password stage",
|
||||
"description": "When set, shows a password field, instead of showing the password field as seaprate step."
|
||||
"description": "When set, shows a password field, instead of showing the password field as separate step."
|
||||
},
|
||||
"case_insensitive_matching": {
|
||||
"type": "boolean",
|
||||
@@ -7207,6 +7207,11 @@
|
||||
"maximum": 2147483647,
|
||||
"title": "Failed attempts before cancel",
|
||||
"description": "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage."
|
||||
},
|
||||
"allow_show_password": {
|
||||
"type": "boolean",
|
||||
"title": "Allow show password",
|
||||
"description": "When enabled, provides a 'show password' button with the password input field."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
2
go.mod
2
go.mod
@@ -28,7 +28,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024061.2
|
||||
goauthentik.io/api/v3 v3.2024061.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -293,8 +293,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
goauthentik.io/api/v3 v3.2024061.2 h1:9NHK2wriMENQHUmbYN3uxsdZZIV0QoEEEaGM0JS8XRY=
|
||||
goauthentik.io/api/v3 v3.2024061.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024061.3 h1:gbP8mhHE2/iCDSZEnAUvRkh9DQhggdTfhsEYKg3sp/U=
|
||||
goauthentik.io/api/v3 v3.2024061.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
||||
6
poetry.lock
generated
6
poetry.lock
generated
@@ -4228,13 +4228,13 @@ websocket-client = ">=1.8.0"
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.9.0"
|
||||
version = "2.10.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee"},
|
||||
{file = "sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6"},
|
||||
{file = "sentry_sdk-2.10.0-py2.py3-none-any.whl", hash = "sha256:87b3d413c87d8e7f816cc9334bff255a83d8b577db2b22042651c30c19c09190"},
|
||||
{file = "sentry_sdk-2.10.0.tar.gz", hash = "sha256:545fcc6e36c335faa6d6cda84669b6e17025f31efbf3b2211ec14efe008b75d1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
28
schema.yml
28
schema.yml
@@ -29993,6 +29993,10 @@ paths:
|
||||
operationId: stages_password_list
|
||||
description: PasswordStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: allow_show_password
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: configure_flow
|
||||
schema:
|
||||
@@ -37067,6 +37071,9 @@ components:
|
||||
nullable: true
|
||||
password_fields:
|
||||
type: boolean
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
default: false
|
||||
application_pre:
|
||||
type: string
|
||||
flow_designation:
|
||||
@@ -37149,7 +37156,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as seaprate step.
|
||||
field as separate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@@ -37217,7 +37224,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as seaprate step.
|
||||
field as separate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@@ -40953,6 +40960,9 @@ components:
|
||||
type: string
|
||||
recovery_url:
|
||||
type: string
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
default: false
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
@@ -41235,6 +41245,10 @@ components:
|
||||
minimum: -2147483648
|
||||
description: How many attempts a user has before the flow is canceled. To
|
||||
lock the user out, use a reputation policy and a user_write stage.
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
description: When enabled, provides a 'show password' button with the password
|
||||
input field.
|
||||
required:
|
||||
- backends
|
||||
- component
|
||||
@@ -41271,6 +41285,10 @@ components:
|
||||
minimum: -2147483648
|
||||
description: How many attempts a user has before the flow is canceled. To
|
||||
lock the user out, use a reputation policy and a user_write stage.
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
description: When enabled, provides a 'show password' button with the password
|
||||
input field.
|
||||
required:
|
||||
- backends
|
||||
- name
|
||||
@@ -42092,7 +42110,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as seaprate step.
|
||||
field as separate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@@ -42804,6 +42822,10 @@ components:
|
||||
minimum: -2147483648
|
||||
description: How many attempts a user has before the flow is canceled. To
|
||||
lock the user out, use a reputation policy and a user_write stage.
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
description: When enabled, provides a 'show password' button with the password
|
||||
input field.
|
||||
PatchedPermissionAssignRequest:
|
||||
type: object
|
||||
description: Request to assign a new permission
|
||||
|
||||
88
tests/wdio/package-lock.json
generated
88
tests/wdio/package-lock.json
generated
@@ -10,8 +10,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@wdio/cli": "^8.39.1",
|
||||
"@wdio/local-runner": "^8.39.1",
|
||||
"@wdio/mocha-framework": "^8.39.0",
|
||||
@@ -943,16 +943,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
|
||||
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
|
||||
"integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.16.0",
|
||||
"@typescript-eslint/type-utils": "7.16.0",
|
||||
"@typescript-eslint/utils": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0",
|
||||
"@typescript-eslint/scope-manager": "7.16.1",
|
||||
"@typescript-eslint/type-utils": "7.16.1",
|
||||
"@typescript-eslint/utils": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -976,15 +976,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
|
||||
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
|
||||
"integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.16.0",
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/typescript-estree": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0",
|
||||
"@typescript-eslint/scope-manager": "7.16.1",
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/typescript-estree": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1004,13 +1004,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
|
||||
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
|
||||
"integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0"
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1021,13 +1021,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
|
||||
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
|
||||
"integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.16.0",
|
||||
"@typescript-eslint/utils": "7.16.0",
|
||||
"@typescript-eslint/typescript-estree": "7.16.1",
|
||||
"@typescript-eslint/utils": "7.16.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1048,9 +1048,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
|
||||
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
|
||||
"integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1061,13 +1061,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
|
||||
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
|
||||
"integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0",
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1113,15 +1113,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
|
||||
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
|
||||
"integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.16.0",
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/typescript-estree": "7.16.0"
|
||||
"@typescript-eslint/scope-manager": "7.16.1",
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/typescript-estree": "7.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1135,12 +1135,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
|
||||
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
|
||||
"integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@wdio/cli": "^8.39.1",
|
||||
"@wdio/local-runner": "^8.39.1",
|
||||
"@wdio/mocha-framework": "^8.39.0",
|
||||
|
||||
@@ -53,7 +53,7 @@ type Pair = [string, string];
|
||||
// Define a getter for each provider type in the radio button collection.
|
||||
|
||||
const providerValues: Pair[] = [
|
||||
["oauth2provider", "oauth2Provider"],
|
||||
["oauth2Provider", "oauth2Provider"],
|
||||
["ldapprovider", "ldapProvider"],
|
||||
["proxyprovider-proxy", "proxyProviderProxy"],
|
||||
["proxyprovider-forwardsingle", "proxyProviderForwardsingle"],
|
||||
@@ -66,7 +66,7 @@ providerValues.forEach(([value, name]: Pair) => {
|
||||
Object.defineProperties(ApplicationWizardView.prototype, {
|
||||
[name]: {
|
||||
get: function () {
|
||||
return this.providerList.$(`>>>input[value="${value}"]`);
|
||||
return this.providerList.$(`>>>div[data-testid=wizard-provider-${value}]`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class ForwardProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import Page from "../page.js";
|
||||
export class LdapForm extends Page {
|
||||
async setBindFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"[name=authorizationFlow]",
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class OauthForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import Page from "../page.js";
|
||||
export class RadiusForm extends Page {
|
||||
async setAuthenticationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class SamlForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class TransparentProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`div*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Page from "./page.js";
|
||||
import UserLibraryPage from "./user-library.page.js";
|
||||
import { $ } from "@wdio/globals";
|
||||
import { and, or, presenceOf, textToBePresentInElement } from "wdio-wait-for";
|
||||
|
||||
/**
|
||||
* sub page containing specific selectors and methods for a specific page
|
||||
@@ -48,6 +49,14 @@ class LoginPage extends Page {
|
||||
await this.pause();
|
||||
await this.password(password);
|
||||
await this.pause();
|
||||
|
||||
const redirect = await $(">>>a[type=submit]");
|
||||
await browser.waitUntil(or(presenceOf(redirect), presenceOf(UserLibraryPage.pageHeader)));
|
||||
|
||||
if (await redirect.isExisting()) {
|
||||
await redirect.click();
|
||||
}
|
||||
|
||||
await this.pause(">>>div.header h1");
|
||||
return UserLibraryPage;
|
||||
}
|
||||
|
||||
@@ -32,10 +32,16 @@ export default class Page {
|
||||
*/
|
||||
|
||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||
const inputBind = await $(searchSelector);
|
||||
const controlSelector = `>>>ak-search-select-view${searchSelector}`;
|
||||
const control = await $(controlSelector);
|
||||
control.scrollIntoView();
|
||||
const inputBind = await control.$(">>>input[type=text]");
|
||||
await inputBind.click();
|
||||
|
||||
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
|
||||
const target = searchBlock.$(buttonSelector);
|
||||
const interior = searchBlock.$(">>>ul");
|
||||
interior.scrollIntoView();
|
||||
const target = interior.$(buttonSelector);
|
||||
return await target.click();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import LoginPage from "../pageobjects/login.page.js";
|
||||
import UserLibraryPage from "../pageobjects/user-library.page.js";
|
||||
import { GOOD_PASSWORD, GOOD_USERNAME } from "./constants.js";
|
||||
import { expect } from "@wdio/globals";
|
||||
|
||||
export const login = async () => {
|
||||
await LoginPage.open();
|
||||
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
|
||||
await expect(UserLibraryPage.pageHeader).toHaveText("My applications");
|
||||
};
|
||||
|
||||
@@ -212,7 +212,9 @@ export const config: Options.Testrunner = {
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
* @param {object} browser instance of created browser/device session
|
||||
*/
|
||||
before: function (_capabilities, _specs) {},
|
||||
before: function (_capabilities, _specs) {
|
||||
browser.setWindowSize(1920, 1080);
|
||||
},
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {string} commandName hook command name
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:lit/recommended",
|
||||
"plugin:custom-elements/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:sonarjs/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"semi": ["error", "always"],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"sonarjs/cognitive-complexity": ["warn", 9],
|
||||
"sonarjs/no-duplicate-string": "off",
|
||||
"sonarjs/no-nested-template-literals": "off"
|
||||
}
|
||||
}
|
||||
375
web/package-lock.json
generated
375
web/package-lock.json
generated
@@ -16,9 +16,10 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/legacy-modes": "^6.4.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.6.1-1720888668",
|
||||
"@goauthentik/api": "^2024.6.1-1721092506",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@@ -48,7 +49,7 @@
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.8",
|
||||
"@babel/core": "^7.24.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
@@ -64,13 +65,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@spotlightjs/spotlight": "^2.0.0",
|
||||
"@storybook/addon-essentials": "^8.2.2",
|
||||
"@storybook/addon-links": "^8.2.2",
|
||||
"@storybook/addon-essentials": "^8.2.4",
|
||||
"@storybook/addon-links": "^8.2.4",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.2.2",
|
||||
"@storybook/web-components": "^8.2.2",
|
||||
"@storybook/web-components-vite": "^8.2.2",
|
||||
"@storybook/manager-api": "^8.2.4",
|
||||
"@storybook/web-components": "^8.2.4",
|
||||
"@storybook/web-components-vite": "^8.2.4",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@@ -92,14 +93,14 @@
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.8",
|
||||
"eslint-plugin-lit": "^1.11.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-sonarjs": "0.25.1",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier": "^3.3.3",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -168,21 +169,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz",
|
||||
"integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==",
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.24.8",
|
||||
"@babel/generator": "^7.24.9",
|
||||
"@babel/helper-compilation-targets": "^7.24.8",
|
||||
"@babel/helper-module-transforms": "^7.24.8",
|
||||
"@babel/helper-module-transforms": "^7.24.9",
|
||||
"@babel/helpers": "^7.24.8",
|
||||
"@babel/parser": "^7.24.8",
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.8",
|
||||
"@babel/types": "^7.24.8",
|
||||
"@babel/types": "^7.24.9",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -198,12 +199,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz",
|
||||
"integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==",
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.9.tgz",
|
||||
"integrity": "sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.24.8",
|
||||
"@babel/types": "^7.24.9",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -363,9 +364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz",
|
||||
"integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==",
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz",
|
||||
"integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.24.7",
|
||||
@@ -2045,9 +2046,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz",
|
||||
"integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==",
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
|
||||
"integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
@@ -3673,9 +3674,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.6.1-1720888668",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
|
||||
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
|
||||
"version": "2024.6.1-1721092506",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
|
||||
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
|
||||
},
|
||||
"node_modules/@hcaptcha/types": {
|
||||
"version": "1.0.3",
|
||||
@@ -6586,9 +6587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-actions": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.2.tgz",
|
||||
"integrity": "sha512-SN4cSRt3f0qXi5te+yhMseSdQuZntA8lGlASbRmN77YQTpIaGsNiH88xFoky0s9qz531hiRfU1R0ZSMylBwSKw==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.4.tgz",
|
||||
"integrity": "sha512-l1dlzWBBkR/5aullsX8N1ZbYr2bkeHPAaMCRy1jG5BBA8IHbi55JFwmJ8XF2gXkT2GyAZnePzb43RuLXz4KxFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -6602,13 +6603,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-backgrounds": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.2.tgz",
|
||||
"integrity": "sha512-m/xJe7uKL+kfJx7pQcHwAeIvJ3tdLIpDGrMAVDNDJHcAxfe44cFjIInaV/1HKf3y5Awap+DZFW66ekkxuI9zzA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.4.tgz",
|
||||
"integrity": "sha512-4oU25rFyr4OgMxHe4RpLJ7lxVwUDfdTi1j/YVyHfYv8koTqjagso8bv0uj0ujP5C3dSsVO0sp3/JOfPDkEUtrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -6620,13 +6621,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-controls": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.2.tgz",
|
||||
"integrity": "sha512-y241aOANGzT5XBADUIvALwG/xF5eC6UItzmWJaFvOzSBCq74GIA0+Hu9atyFdvFQbXOrdvPWC4jR+9iuBFRxAA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.4.tgz",
|
||||
"integrity": "sha512-e56aUYhxyR8zJJstRAUP3WILhWTcvgRf5bysTtiyjFAL7U47cuCr043+IYEsxLkXhuZTKX2pcYSrjBtT5bYkVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.2",
|
||||
@@ -6638,21 +6639,21 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.2.tgz",
|
||||
"integrity": "sha512-qk/yjAR9RpsSrKLLbeCgb6u58c8TmYqyJSnXgbAozZZNKHBWlIpvZ/hTNYud8qo0coPlxnLdjnZf32TykWGlAg==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.4.tgz",
|
||||
"integrity": "sha512-oyrDw4nGfntu5Hkhr2Qt1wUOyLaVVERQekYyejyir92QhM10UeA7ZarPXNLfCTj7rbTrWmM1Waka9Tsf8TGMrw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/blocks": "8.2.2",
|
||||
"@storybook/csf-plugin": "8.2.2",
|
||||
"@storybook/blocks": "8.2.4",
|
||||
"@storybook/csf-plugin": "8.2.4",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "8.2.2",
|
||||
"@storybook/react-dom-shim": "8.2.4",
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
@@ -6666,7 +6667,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/fs-extra": {
|
||||
@@ -6684,20 +6685,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-essentials": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.2.tgz",
|
||||
"integrity": "sha512-yN//BFMbSvNV0+Sll2hcKmgJX06TUKQDm6pZimUjkXczFtOmK7K/UdDmKjWS+qjhfJdWpxdRoEpxoHvvRmNfsA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.4.tgz",
|
||||
"integrity": "sha512-4upNauDJAJxauxnoUpUvzDnLo18C2yTVxgg+Id9wrKpt9C+CYH2oXyXzxoYGucYWZEe7zgCO6rWrGrKEisiLPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/addon-actions": "8.2.2",
|
||||
"@storybook/addon-backgrounds": "8.2.2",
|
||||
"@storybook/addon-controls": "8.2.2",
|
||||
"@storybook/addon-docs": "8.2.2",
|
||||
"@storybook/addon-highlight": "8.2.2",
|
||||
"@storybook/addon-measure": "8.2.2",
|
||||
"@storybook/addon-outline": "8.2.2",
|
||||
"@storybook/addon-toolbars": "8.2.2",
|
||||
"@storybook/addon-viewport": "8.2.2",
|
||||
"@storybook/addon-actions": "8.2.4",
|
||||
"@storybook/addon-backgrounds": "8.2.4",
|
||||
"@storybook/addon-controls": "8.2.4",
|
||||
"@storybook/addon-docs": "8.2.4",
|
||||
"@storybook/addon-highlight": "8.2.4",
|
||||
"@storybook/addon-measure": "8.2.4",
|
||||
"@storybook/addon-outline": "8.2.4",
|
||||
"@storybook/addon-toolbars": "8.2.4",
|
||||
"@storybook/addon-viewport": "8.2.4",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -6705,13 +6706,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-highlight": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.2.tgz",
|
||||
"integrity": "sha512-yDTRzzL+IJAymgY32xoZl09BGBVmPOUV2wVNGYcZkkBLvz2GSQMTfUe1/7F4jAx//+rFBu48/MQzsTC7Bk8kPw==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.4.tgz",
|
||||
"integrity": "sha512-Ll/2y0m/q9ko9jFt40qsiee4fds6vpcwwxi3mPAVwRV/J7PpMzPkoLxM54bKpeHiWdTeGCXRguXNvyeQMQf3pg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
@@ -6721,13 +6722,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.2.tgz",
|
||||
"integrity": "sha512-eGh7O7SgTJMtnuXC0HlRPOegu1njcJS2cnVqjbzjvjxsPSBhbHpdYMi9Q9E7al/FKuqMUOjIR9YLIlmK1AJaqA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.4.tgz",
|
||||
"integrity": "sha512-1FgD6YXdXXSEDrp2aO4LxYt/X7LnBYx7cLlFla+xbn1CZLGqWLLeOT+BFd29wxpzs3u1Tap9r1iz1vRYL5ziyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@@ -6740,7 +6741,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -6749,9 +6750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-measure": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.2.tgz",
|
||||
"integrity": "sha512-3rCo/aMltt5FrBVdr2dYlD8HlE2q9TLKGJZnwh9on4QyL6ArHbdYw0LmyHe/LrFahJ49w1XQZBMSJcAdRkkS7w==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.4.tgz",
|
||||
"integrity": "sha512-bSyE3mGDaaIKoe6Kt/f20YXKsn8WSoJUHrfKA68gbb+H3tegVQaqeS2KY5YzLqvjHe1qSmrO132NJt8RixLOPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -6762,13 +6763,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-outline": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.2.tgz",
|
||||
"integrity": "sha512-Y+PQtfTNO8GLX5nz+3x5AMfHNvdGvBXazJ29+Rl1ygYN1+Q9ZhRJDE1kAK0wLxb7CG14peAgdYEaQb3Rduv7HQ==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.4.tgz",
|
||||
"integrity": "sha512-1C6NrvSDREgCZ7o/1n7Ca81uDDzrSrzWiOkh4OeA7PPQ/445cAOX2OMvxzNkKDIT9GLCLNi9M5XIVyGxJVS4dQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -6779,26 +6780,26 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-toolbars": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.2.tgz",
|
||||
"integrity": "sha512-JGOueOc3EPljlCl9dVSQee0aMYoqGNvN0UH+R6wYJ3bDZ+tUG/iYpsZVPUOvS8vzp3Imk5Is1kzQbQYJtzdGLg==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.4.tgz",
|
||||
"integrity": "sha512-iPnSr+hdz40Uoqg2cimyWf01/Y8GdgdMKB+b47TGIxtn9SEFBXck00ZG8ttwBvEsecu9K9CDt20fIOnr6oK5tQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-viewport": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.2.tgz",
|
||||
"integrity": "sha512-gkZ8bsjGGP0NuevkT2iKC+szezSy+w4BrBDknf490mRU2K/B2e7TGojf/j/AtxzILMzD4IKzKUXbE/zwcqjZvA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.4.tgz",
|
||||
"integrity": "sha512-58DcoX0xGpWlJfc0iLDjggkVPYzT4JdCZA2ioK9SQXQMsUzGFwR5PAAJv1tivYp7467tNkXvcM3QTb3Q3g8p4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"memoizerific": "^1.11.3"
|
||||
@@ -6808,7 +6809,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addons": {
|
||||
@@ -7101,9 +7102,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/blocks": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.2.tgz",
|
||||
"integrity": "sha512-av0Tryg4toDl2L/d1ABErtsAk9wvM1su6+M4wq5/Go50sk5IjGTldhbZFa9zNOohxLkZwaj0Q5xAgJ1Y+m5KrQ==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.4.tgz",
|
||||
"integrity": "sha512-Hl2Dpg41YiJLSVXxjEJPjgPShrDJM3RY6HEEOjqTcAADsheX1IHAWXMJSJGMmne3Sew6VdJXPuHBIOFV4suZxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@@ -7128,7 +7129,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -7140,12 +7141,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.2.tgz",
|
||||
"integrity": "sha512-tyt+CjzLEuRHU2NERZSy7JfnTpTJo10HrRysJcRtzclu3TOzx7bWszUJRHho9ttyypBX6w5+8TPcqXh/vu0tig==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.4.tgz",
|
||||
"integrity": "sha512-hDx0ZLcnFrIJaVoFMu41d9w1uWmwy/DDUuIbSd0T7xHwWyVqgI8lmaQlBIp81/QmSKaUB964UduHcdIjkoWoYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "8.2.2",
|
||||
"@storybook/csf-plugin": "8.2.4",
|
||||
"@types/find-cache-dir": "^3.2.1",
|
||||
"browser-assert": "^1.2.1",
|
||||
"es-module-lexer": "^1.5.0",
|
||||
@@ -7161,7 +7162,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@preact/preset-vite": "*",
|
||||
"storybook": "^8.2.2",
|
||||
"storybook": "^8.2.4",
|
||||
"typescript": ">= 4.3.x",
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite-plugin-glimmerx": "*"
|
||||
@@ -7234,15 +7235,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/codemod": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.2.tgz",
|
||||
"integrity": "sha512-wRUVKLHVUhbLJYKW3QOufUxJGwaUT4jTCD8+HOGpHPdJO3NrwXu186xt4tuPZO2Y/NnacPeCQPsaK5ok4O8o7A==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.4.tgz",
|
||||
"integrity": "sha512-QcZdqjX4NvkVcWR3yI9it3PfqmBOCR+3iY6j4PmG7p5IE0j9kXMKBbeFrBRprSijHKlwcjbc3bRx2SnKF6AFEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@storybook/core": "8.2.2",
|
||||
"@storybook/core": "8.2.4",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"cross-spawn": "^7.0.3",
|
||||
@@ -7332,9 +7333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.2.tgz",
|
||||
"integrity": "sha512-L4ojYI+Os/i5bCReDIlFgEDQSS94mbJlNU9WRzEGZpqNC5/hbFEC9Tip7P1MiRx9NrewkzU7b+UCP7mi3e4drQ==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.4.tgz",
|
||||
"integrity": "sha512-jePmsGZT2hhUNQs8ED6+hFVt2m4hrMseO8kkN7Mcsve1MIujzHUS7Gjo4uguBwHJJOtiXB2fw4OSiQCmsXscZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@@ -7456,9 +7457,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.2.tgz",
|
||||
"integrity": "sha512-3K2RUpDDvq3DT46qAIj2VBC+fzTTebRUcZUsRfS6G1AzaX9p25iClEHiwcJacFkgQKhkci8A/Ly3Z4JJ3b4Pgw==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.4.tgz",
|
||||
"integrity": "sha512-7V2tmeyAwv4/AQiBpB+7fCpphnY1yhcz+Zv9esUOHKqFn5+7u9FKpEXFFcf6fcbqXr2KoNw2F1EnTv3K/SxXrg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"unplugin": "^1.3.1"
|
||||
@@ -7468,7 +7469,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/global": {
|
||||
@@ -7489,107 +7490,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/manager-api": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.2.2.tgz",
|
||||
"integrity": "sha512-v7pbddJO21RAsGyT0+GZMgP25nLCdhQFYnmy+aRCgL6rz+k7bToPwcL+qK0mb5sfng+Ah2MAAK9ZvXWTYAVeqw==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.2.4.tgz",
|
||||
"integrity": "sha512-ayiOtcGupSeLCi2doEsRpALNPo4MBWYruc+e3jjkeVJQIg9A1ipSogNQh8unuOmq9rezO4/vcNBd6MxLs3xLWg==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.1.11.tgz",
|
||||
"integrity": "sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.2.4.tgz",
|
||||
"integrity": "sha512-IxOiUYYzNnk1OOz3zQBhsa3P1fsgqeMBZcH7TjiQWs9osuWG20oqsFR6+Z3dxoW8IuQHvpnREGKvAbRsDsThcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/channels": "8.1.11",
|
||||
"@storybook/client-logger": "8.1.11",
|
||||
"@storybook/core-events": "8.1.11",
|
||||
"@storybook/csf": "^0.1.7",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/types": "8.1.11",
|
||||
"@types/qs": "^6.9.5",
|
||||
"dequal": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"memoizerific": "^1.11.3",
|
||||
"qs": "^6.10.0",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"ts-dedent": "^2.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api/node_modules/@storybook/channels": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.11.tgz",
|
||||
"integrity": "sha512-fu5FTqo6duOqtJFa6gFzKbiSLJoia+8Tibn3xFfB6BeifWrH81hc+AZq0lTmHo5qax2G5t8ZN8JooHjMw6k2RA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/client-logger": "8.1.11",
|
||||
"@storybook/core-events": "8.1.11",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"telejson": "^7.2.0",
|
||||
"tiny-invariant": "^1.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api/node_modules/@storybook/client-logger": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.1.11.tgz",
|
||||
"integrity": "sha512-DVMh2usz3yYmlqCLCiCKy5fT8/UR9aTh+gSqwyNFkGZrIM4otC5A8eMXajXifzotQLT5SaOEnM3WzHwmpvMIEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api/node_modules/@storybook/core-events": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.11.tgz",
|
||||
"integrity": "sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "^0.1.7",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api/node_modules/@storybook/types": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.1.11.tgz",
|
||||
"integrity": "sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/channels": "8.1.11",
|
||||
"@types/express": "^4.7.0",
|
||||
"file-system-cache": "2.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.2.2.tgz",
|
||||
"integrity": "sha512-4fb1/yT9WXHzHjs0In6orIEZxga5eXd9UaXEFGudBgowCjDUVP9LabDdKTbGusz20lfaAkATsRG/W+EcSLoh8w==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.2.4.tgz",
|
||||
"integrity": "sha512-p2ypPWuKKFY/ij7yYjvdnrOcfdpxnAJd9D4/2Hm2eVioE4y8HQSND54t9OfkW+498Ez7ph4zW9ez005XqzH/+w==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -7598,7 +7527,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/theming": {
|
||||
@@ -7649,12 +7578,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.2.2.tgz",
|
||||
"integrity": "sha512-6lFesQw9TmdbzgFRytlNqQDPgEqlCQzOvW91ZDyDBdytN64XXONYfNBJqef0tM3hLcXBv1vNIzlnOsMRDhIhZQ==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.2.4.tgz",
|
||||
"integrity": "sha512-S1ggBI9x+RjUj/iUCOJuW7emf+PnkslHUrfTpsmmlKqDGdSMJoqH7eZiFRQ0B/p/aT+IU3jRnCSsjF4N5eDHLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/components": "^8.2.4",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/manager-api": "^8.2.4",
|
||||
"@storybook/preview-api": "^8.2.4",
|
||||
"@storybook/theming": "^8.2.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
@@ -7667,17 +7600,17 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^2.0.0 || ^3.0.0",
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components-vite": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.2.2.tgz",
|
||||
"integrity": "sha512-e+yGo31hNW2uw+ujl9sNLju/JBwCCvOCaqO1Pk+MDy0Kz3xGbr+4RHRbRQ+DyngLdvUZIzy/gypd/Im4wPyf0Q==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.2.4.tgz",
|
||||
"integrity": "sha512-zJwhlYgoMwPHiM7ySLOgTDuNBDH3qPmi+qrvtdpEGVdrSIvijx5jsQQz4XTP2b6BXyOg1g9VaMfQ5S8LaSZ74A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/builder-vite": "8.2.2",
|
||||
"@storybook/web-components": "8.2.2",
|
||||
"@storybook/builder-vite": "8.2.4",
|
||||
"@storybook/web-components": "8.2.4",
|
||||
"magic-string": "^0.30.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7688,7 +7621,33 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.2"
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components/node_modules/@storybook/components": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.2.4.tgz",
|
||||
"integrity": "sha512-JLT1RoR/RXX+ZTeFoY85CRHb9Zz3l0PRRUSetEjoIJdnBGeL5C38bs0s9QnYjpCDLUlhdYhTln+GzmbyH8ocpA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components/node_modules/@storybook/theming": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.2.4.tgz",
|
||||
"integrity": "sha512-B4HQMzTeg1TgV9uPDIoDkMSnP839Y05I9+Tw60cilAD+jTqrCvMlccHfehsTzJk+gioAflunATcbU05TMZoeIQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@@ -14463,8 +14422,9 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-sonarjs": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz",
|
||||
"integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==",
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-only",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
@@ -20552,9 +20512,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -22412,15 +22373,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.2.tgz",
|
||||
"integrity": "sha512-xDT9gyzAEFQNeK7P+Mj/8bNzN+fbm6/4D6ihdSzmczayjydpNjMs74HDHMY6S4Bfu6tRVyEK2ALPGnr6ZVofBA==",
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.4.tgz",
|
||||
"integrity": "sha512-ASavW8vIHiWpFY+4M6ngeqK5oL4OkxqdpmQYxvRqH0gA1G1hfq/vmDw4YC4GnqKwyWPQh2kaV5JFurKZVaeaDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@storybook/codemod": "8.2.2",
|
||||
"@storybook/core": "8.2.2",
|
||||
"@storybook/codemod": "8.2.4",
|
||||
"@storybook/core": "8.2.4",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@yarnpkg/fslib": "2.10.3",
|
||||
"@yarnpkg/libzip": "2.3.0",
|
||||
|
||||
@@ -43,9 +43,10 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/legacy-modes": "^6.4.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.6.1-1720888668",
|
||||
"@goauthentik/api": "^2024.6.1-1721092506",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@@ -75,7 +76,7 @@
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.8",
|
||||
"@babel/core": "^7.24.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
@@ -91,13 +92,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@spotlightjs/spotlight": "^2.0.0",
|
||||
"@storybook/addon-essentials": "^8.2.2",
|
||||
"@storybook/addon-links": "^8.2.2",
|
||||
"@storybook/addon-essentials": "^8.2.4",
|
||||
"@storybook/addon-links": "^8.2.4",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.2.2",
|
||||
"@storybook/web-components": "^8.2.2",
|
||||
"@storybook/web-components-vite": "^8.2.2",
|
||||
"@storybook/manager-api": "^8.2.4",
|
||||
"@storybook/web-components": "^8.2.4",
|
||||
"@storybook/web-components-vite": "^8.2.4",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@@ -119,14 +120,14 @@
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.8",
|
||||
"eslint-plugin-lit": "^1.11.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-sonarjs": "0.25.1",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier": "^3.3.3",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -5,9 +5,9 @@ import process from "process";
|
||||
|
||||
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
|
||||
|
||||
function compareXlfAndSrc(loc) {
|
||||
const xlf = path.join("./xliff", `${loc}.xlf`);
|
||||
const src = path.join("./src/locales", `${loc}.ts`);
|
||||
function generatedFileIsUpToDateWithXliffSource(loc) {
|
||||
const xliff = path.join("./xliff", `${loc}.xlf`);
|
||||
const gened = path.join("./src/locales", `${loc}.ts`);
|
||||
|
||||
// Returns false if: the expected XLF file doesn't exist, The expected
|
||||
// generated file doesn't exist, or the XLF file is newer (has a higher date)
|
||||
@@ -15,29 +15,28 @@ function compareXlfAndSrc(loc) {
|
||||
// generates a unique error message and halts the build.
|
||||
|
||||
try {
|
||||
var xlfStat = fs.statSync(xlf);
|
||||
var xlfStat = fs.statSync(xliff);
|
||||
} catch (_error) {
|
||||
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If the generated file doesn't exist, of course it's not up to date.
|
||||
try {
|
||||
var srcStat = fs.statSync(src);
|
||||
var genedStat = fs.statSync(gened);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the xlf is newer (greater) than src, it's out of date.
|
||||
if (xlfStat.mtimeMs > srcStat.mtimeMs) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// if the generated file is the same age or older (date is greater) than the xliff file, it's
|
||||
// presumed to have been generated by that file and is up-to-date.
|
||||
return genedStat.mtimeMs >= xlfStat.mtimeMs;
|
||||
}
|
||||
|
||||
// For all the expected files, find out if any aren't up-to-date.
|
||||
|
||||
const upToDate = localizeRules.targetLocales.reduce(
|
||||
(acc, loc) => acc && compareXlfAndSrc(loc),
|
||||
(acc, loc) => acc && generatedFileIsUpToDateWithXliffSource(loc),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -61,7 +60,9 @@ if (!upToDate) {
|
||||
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
|
||||
.join("\n");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Translation tables rebuilt.\n${report}\n`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Locale ./src is up-to-date");
|
||||
|
||||
@@ -12,4 +12,5 @@ const cmd = [
|
||||
"-S './src/locales/**' ./src -s",
|
||||
].join(" ");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(execSync(cmd, { encoding: "utf8" }));
|
||||
|
||||
@@ -43,34 +43,53 @@ const eslintConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const porcelainV1 = /^(..)\s+(.*$)/;
|
||||
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
|
||||
function findChangedFiles() {
|
||||
const porcelainV1 = /^(..)\s+(.*$)/;
|
||||
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
|
||||
|
||||
const statuses = gitStatus.split("\n").reduce((acc, line) => {
|
||||
const match = porcelainV1.exec(line.replace("\n"));
|
||||
if (!match) {
|
||||
return acc;
|
||||
}
|
||||
const [status, path] = Array.from(match).slice(1, 3);
|
||||
return [...acc, [status, path.split("\x00")[0]]];
|
||||
}, []);
|
||||
const statuses = gitStatus.split("\n").reduce((acc, line) => {
|
||||
const match = porcelainV1.exec(line.replace("\n"));
|
||||
if (!match) {
|
||||
return acc;
|
||||
}
|
||||
const [status, path] = Array.from(match).slice(1, 3);
|
||||
return [...acc, [status, path.split("\x00")[0]]];
|
||||
}, []);
|
||||
|
||||
const isModified = /^(M|\?|\s)(M|\?|\s)/;
|
||||
const modified = (s) => isModified.test(s);
|
||||
const isModified = /^(M|\?|\s)(M|\?|\s)/;
|
||||
const modified = (s) => isModified.test(s);
|
||||
|
||||
const isCheckable = /\.(ts|js|mjs)$/;
|
||||
const checkable = (s) => isCheckable.test(s);
|
||||
const isCheckable = /\.(ts|js|mjs)$/;
|
||||
const checkable = (s) => isCheckable.test(s);
|
||||
|
||||
const ignored = /\/\.storybook\//;
|
||||
const notIgnored = (s) => !ignored.test(s);
|
||||
const ignored = /\/\.storybook\//;
|
||||
const notIgnored = (s) => !ignored.test(s);
|
||||
|
||||
const updated = statuses.reduce(
|
||||
(acc, [status, filename]) =>
|
||||
modified(status) && checkable(filename) && notIgnored(filename)
|
||||
? [...acc, path.join(projectRoot, filename)]
|
||||
: acc,
|
||||
[],
|
||||
);
|
||||
return statuses.reduce(
|
||||
(acc, [status, filename]) =>
|
||||
modified(status) && checkable(filename) && notIgnored(filename)
|
||||
? [...acc, path.join(projectRoot, filename)]
|
||||
: acc,
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Run eslint with extra-hard checks
|
||||
|
||||
options:
|
||||
-c, --changed: (default) check only the files that have changed
|
||||
-n, --nightmare: check all the files in the repository
|
||||
-h, --help: This help message
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const updated =
|
||||
process.argv.length > 2 && (process.argv[2] === "-n" || process.argv[2] === "--nightmare")
|
||||
? ["./src/", "./build.mjs", "./scripts/*.mjs"]
|
||||
: findChangedFiles();
|
||||
|
||||
const eslint = new ESLint(eslintConfig);
|
||||
const results = await eslint.lintFiles(updated);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
|
||||
|
||||
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2&>1) ]]; then
|
||||
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
|
||||
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
|
||||
|
||||
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
|
||||
|
||||
6
web/sfe/package-lock.json
generated
6
web/sfe/package-lock.json
generated
@@ -28,9 +28,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.6.1-1720888668",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
|
||||
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
|
||||
"version": "2024.6.1-1721092506",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
|
||||
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
|
||||
@@ -151,7 +151,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
|
||||
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
|
||||
const properties = Array.isArray(attributes)
|
||||
? { ".activeWhen": attributes }
|
||||
: attributes ?? {};
|
||||
: (attributes ?? {});
|
||||
if (path) {
|
||||
properties["path"] = path;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type LocalTypeCreate = TypeCreate & {
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
note?: ProviderNote;
|
||||
testId: string;
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
@@ -46,6 +47,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-oauth2Provider",
|
||||
iconUrl: "/static/authentik/sources/openidconnect.svg",
|
||||
},
|
||||
{
|
||||
@@ -62,6 +64,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-ldapprovider",
|
||||
iconUrl: "/static/authentik/sources/ldap.png",
|
||||
},
|
||||
{
|
||||
@@ -77,6 +80,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.Proxy,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-proxy",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@@ -92,6 +96,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.ForwardSingle,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-forwardsingle",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@@ -107,6 +112,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.ForwardDomain,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-forwarddomain",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@@ -123,6 +129,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
note: () => html`<ak-license-notice></ak-license-notice>`,
|
||||
requiresEnterprise: true,
|
||||
component: "",
|
||||
testId: "wizard-provider-racprovider",
|
||||
iconUrl: "/static/authentik/sources/rac.svg",
|
||||
},
|
||||
{
|
||||
@@ -137,6 +144,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-samlprovider",
|
||||
iconUrl: "/static/authentik/sources/saml.png",
|
||||
},
|
||||
{
|
||||
@@ -151,6 +159,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as RadiusProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-radiusprovider",
|
||||
iconUrl: "/static/authentik/sources/radius.svg",
|
||||
},
|
||||
{
|
||||
@@ -165,6 +174,7 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as SCIMProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-scimprovider",
|
||||
iconUrl: "/static/authentik/sources/scim.png",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -103,11 +103,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
);
|
||||
if (!providerModel) {
|
||||
throw new Error(
|
||||
`Could not determine provider model from user request: ${JSON.stringify(
|
||||
this.wizard,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +114,6 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
};
|
||||
|
||||
this.send(request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@@ -66,14 +66,11 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
renderExampleButtons(): TemplateResult {
|
||||
const header = html`<p>${msg("Example context data")}</p>`;
|
||||
switch (this.mapping?.metaModelName) {
|
||||
case "authentik_sources_ldap.ldappropertymapping":
|
||||
return html`${header}${this.renderExampleLDAP()}`;
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
renderExampleButtons() {
|
||||
return this.mapping?.metaModelName === "authentik_sources_ldap.ldappropertymapping"
|
||||
? html`<p>${msg("Example context data")}</p>
|
||||
${this.renderExampleLDAP()}`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
renderExampleLDAP(): TemplateResult {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@@ -9,7 +9,6 @@ import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
BackendsEnum,
|
||||
@@ -72,10 +71,10 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
|
||||
return html` <span>
|
||||
${msg("Validate the user's password against the selected backend(s).")}
|
||||
</span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name || "")}"
|
||||
value="${this.instance?.name || ""}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
@@ -158,7 +157,7 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.failedAttemptsBeforeCancel, 5)}"
|
||||
value="${this.instance?.failedAttemptsBeforeCancel ?? 5}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
@@ -168,6 +167,12 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-switch-input
|
||||
name="allowShowPassword"
|
||||
label="Allow Show Password"
|
||||
?checked=${this.instance?.allowShowPassword ?? false}
|
||||
help=${msg("Provide users with a 'show password' button.")}
|
||||
></ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,11 @@ export function transformCredentialCreateOptions(
|
||||
user.id = u8arr(b64enc(u8arr(stringId)));
|
||||
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
||||
|
||||
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
|
||||
return {
|
||||
...credentialCreateOptions,
|
||||
challenge,
|
||||
user,
|
||||
});
|
||||
|
||||
return transformedCredentialCreateOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Assertion {
|
||||
@@ -98,12 +97,11 @@ export function transformCredentialRequestOptions(
|
||||
},
|
||||
);
|
||||
|
||||
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
|
||||
return {
|
||||
...credentialRequestOptions,
|
||||
challenge,
|
||||
allowCredentials,
|
||||
});
|
||||
|
||||
return transformedCredentialRequestOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthAssertion {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { TemplateResult, css, html, nothing } from "lit";
|
||||
@@ -25,7 +25,7 @@ const selectStyles = css`
|
||||
* @part select - The select itself, to override the height specified above.
|
||||
*/
|
||||
@customElement("ak-multi-select")
|
||||
export class AkMultiSelect extends AKElement {
|
||||
export class AkMultiSelect extends AkControlElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataset.akControl = "true";
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { groupBy } from "@goauthentik/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>`,
|
||||
);
|
||||
};
|
||||
20
web/src/elements/AkControlElement.ts
Normal file
20
web/src/elements/AkControlElement.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AKElement } from "./Base";
|
||||
|
||||
/**
|
||||
* @class - prototype for all of our hand-made input elements
|
||||
*
|
||||
* Ensures that the `data-ak-control` property is always set, so that
|
||||
* scrapers can find it easily, and adds a corresponding method for
|
||||
* extracting the value.
|
||||
*
|
||||
*/
|
||||
export class AkControlElement extends AKElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataset.akControl = "true";
|
||||
}
|
||||
|
||||
json() {
|
||||
throw new Error("Controllers using this protocol must override this method");
|
||||
}
|
||||
}
|
||||
@@ -92,11 +92,13 @@ export class Tabs extends AKElement {
|
||||
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
|
||||
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
|
||||
const params = getURLParams();
|
||||
if (this.pageIdentifier in params && !this.currentPage) {
|
||||
if (this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null) {
|
||||
// To update the URL to match with the current slot
|
||||
this.onClick(params[this.pageIdentifier] as string);
|
||||
}
|
||||
if (
|
||||
this.pageIdentifier in params &&
|
||||
!this.currentPage &&
|
||||
this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null
|
||||
) {
|
||||
// To update the URL to match with the current slot
|
||||
this.onClick(params[this.pageIdentifier] as string);
|
||||
}
|
||||
}
|
||||
if (!this.currentPage) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -23,7 +23,7 @@ function* kvToPairs(items: CheckboxPair[]): Iterable<CheckboxPr> {
|
||||
}
|
||||
}
|
||||
|
||||
const AkElementWithCustomEvents = CustomEmitterElement(AKElement);
|
||||
const AkElementWithCustomEvents = CustomEmitterElement(AkControlElement);
|
||||
|
||||
/**
|
||||
* @element ak-checkbox-group
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import { debounce } from "@goauthentik/elements/utils/debounce";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
@@ -26,9 +26,8 @@ import type { DataProvider, DualSelectPair } from "./types";
|
||||
*/
|
||||
|
||||
@customElement("ak-dual-select-provider")
|
||||
export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* A function that takes a page and returns the DualSelectPair[] collection with which to update
|
||||
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
|
||||
/** A function that takes a page and returns the DualSelectPair[] collection with which to update
|
||||
* the "Available" pane.
|
||||
*
|
||||
* @attr
|
||||
@@ -84,8 +83,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
||||
constructor() {
|
||||
super();
|
||||
setTimeout(() => this.fetch(1), 0);
|
||||
// Notify AkForElementHorizontal how to handle this thing.
|
||||
this.dataset.akControl = "true";
|
||||
this.onNav = this.onNav.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
|
||||
@@ -74,8 +73,8 @@ export function serializeForm<T extends KeyUnknown>(
|
||||
return;
|
||||
}
|
||||
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (element.hidden || !inputElement) {
|
||||
const inputElement = element.querySelector<AkControlElement>("[name]");
|
||||
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,10 +83,6 @@ export function serializeForm<T extends KeyUnknown>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip elements that are writeOnly where the user hasn't clicked on the value
|
||||
if (element.writeOnly && !element.writeOnlyActivated) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
inputElement.tagName.toLowerCase() === "select" &&
|
||||
"multiple" in inputElement.attributes
|
||||
@@ -120,17 +115,6 @@ export function serializeForm<T extends KeyUnknown>(
|
||||
assignValue(inputElement, inputElement.checked, json);
|
||||
} else if ("selectedFlow" in inputElement) {
|
||||
assignValue(inputElement, inputElement.value, json);
|
||||
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = inputElement as unknown as SearchSelect<unknown>;
|
||||
try {
|
||||
const value = select.toForm();
|
||||
assignValue(inputElement, value, json);
|
||||
} catch (exc) {
|
||||
if (exc instanceof PreventFormSubmit) {
|
||||
throw new PreventFormSubmit(exc.message, element);
|
||||
}
|
||||
throw exc;
|
||||
}
|
||||
} else {
|
||||
assignValue(inputElement, inputElement.value, json);
|
||||
}
|
||||
|
||||
142
web/src/elements/forms/SearchSelect/SearchKeyboardController.ts
Normal file
142
web/src/elements/forms/SearchSelect/SearchKeyboardController.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { LitElement, ReactiveController, ReactiveControllerHost } from "lit";
|
||||
|
||||
import {
|
||||
KeyboardControllerCloseEvent,
|
||||
KeyboardControllerSelectEvent,
|
||||
} from "./SearchKeyboardControllerEvents.js";
|
||||
|
||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & LitElement & { value?: string };
|
||||
type ValuedHtmlElement = HTMLElement & { value: string };
|
||||
|
||||
/**
|
||||
* @class AkKeyboardController
|
||||
*
|
||||
* This reactive controller connects to the host and sets up listeners for keyboard events to manage
|
||||
* a list of elements. Navigational controls (up, down, home, end) do what you'd expect. Enter and Space
|
||||
* "select" the current item, which means:
|
||||
*
|
||||
* - All other items lose focus and tabIndex
|
||||
* - The selected item gains focus and tabIndex
|
||||
* - The value of the selected item is sent to the host as an event
|
||||
*
|
||||
* @fires ak-keyboard-controller-select - When an element is selected. Contains the `value` of the
|
||||
* selected item.
|
||||
*
|
||||
* @fires ak-keyboard-controller-close - When `Escape` is pressed. Clients can do with this as they
|
||||
* wish.
|
||||
*
|
||||
*/
|
||||
export class AkKeyboardController implements ReactiveController {
|
||||
private host: ReactiveElementHost;
|
||||
|
||||
private index: number = 0;
|
||||
|
||||
private selector: string;
|
||||
|
||||
private highlighter: string;
|
||||
|
||||
private items: ValuedHtmlElement[] = [];
|
||||
|
||||
/**
|
||||
* @arg selector: The class identifier (it *must* be a class identifier) of the DOM objects
|
||||
* that this controller will be working with.
|
||||
*
|
||||
* NOTE: The objects identified by the selector *must* have a `value` associated with them, and
|
||||
* as in all things HTML, that value must be a string.
|
||||
*
|
||||
* @arg highlighter: The class identifier that clients *may* use to set an alternative focus
|
||||
* on the object. Note that the object will always receive focus.
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
host: ReactiveElementHost,
|
||||
selector = ".ak-select-item",
|
||||
highlighter = ".ak-highlight-item",
|
||||
) {
|
||||
this.host = host;
|
||||
host.addController(this);
|
||||
this.selector = selector[0] === "." ? selector : `.${selector}`;
|
||||
this.highlighter = highlighter.replace(/^\./, "");
|
||||
}
|
||||
|
||||
hostUpdated() {
|
||||
this.items = Array.from(this.host.renderRoot.querySelectorAll(this.selector));
|
||||
const current = this.items.findIndex((item) => item.value === this.host.value);
|
||||
if (current >= 0) {
|
||||
this.index = current;
|
||||
}
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.addEventListener("keydown", this.onKeydown);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.host.removeEventListener("keydown", this.onKeydown);
|
||||
}
|
||||
|
||||
hostVisible() {
|
||||
this.items[this.index].focus();
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.items[this.index];
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.current?.value;
|
||||
}
|
||||
|
||||
set value(v: string) {
|
||||
const index = this.items.findIndex((i) => i.value === v);
|
||||
if (index !== undefined) {
|
||||
this.index = index;
|
||||
this.performUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private performUpdate() {
|
||||
const items = this.items;
|
||||
items.forEach((item) => {
|
||||
item.classList.remove(this.highlighter);
|
||||
item.tabIndex = -1;
|
||||
});
|
||||
items[this.index].classList.add(this.highlighter);
|
||||
items[this.index].tabIndex = 0;
|
||||
items[this.index].focus();
|
||||
}
|
||||
|
||||
@bound
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
const key = event.key;
|
||||
match({ key })
|
||||
.with({ key: "ArrowDown" }, () => {
|
||||
this.index = Math.min(this.index + 1, this.items.length - 1);
|
||||
this.performUpdate();
|
||||
})
|
||||
.with({ key: "ArrowUp" }, () => {
|
||||
this.index = Math.max(this.index - 1, 0);
|
||||
this.performUpdate();
|
||||
})
|
||||
.with({ key: "Home" }, () => {
|
||||
this.index = 0;
|
||||
this.performUpdate();
|
||||
})
|
||||
.with({ key: "End" }, () => {
|
||||
this.index = this.items.length - 1;
|
||||
this.performUpdate();
|
||||
})
|
||||
.with({ key: " " }, () => {
|
||||
this.host.dispatchEvent(new KeyboardControllerSelectEvent(this.value));
|
||||
})
|
||||
.with({ key: "Enter" }, () => {
|
||||
this.host.dispatchEvent(new KeyboardControllerSelectEvent(this.value));
|
||||
})
|
||||
.with({ key: "Escape" }, () => {
|
||||
this.host.dispatchEvent(new KeyboardControllerCloseEvent());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export class KeyboardControllerSelectEvent extends Event {
|
||||
value: string | undefined;
|
||||
constructor(value: string | undefined) {
|
||||
super("ak-keyboard-controller-select", { composed: true, bubbles: true });
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardControllerCloseEvent extends Event {
|
||||
constructor() {
|
||||
super("ak-keyboard-controller-close", { composed: true, bubbles: true });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"ak-keyboard-controller-select": KeyboardControllerSelectEvent;
|
||||
"ak-keyboard-controller-close": KeyboardControllerCloseEvent;
|
||||
}
|
||||
}
|
||||
63
web/src/elements/forms/SearchSelect/SearchSelectEvents.ts
Normal file
63
web/src/elements/forms/SearchSelect/SearchSelectEvents.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* class SearchSelectSelectEvent
|
||||
*
|
||||
* Intended meaning: the user selected an item from the entire dialogue, either by clicking on it
|
||||
* with the mouse, or selecting it with the keyboard controls and pressing Enter or Space.
|
||||
*/
|
||||
export class SearchSelectSelectEvent extends Event {
|
||||
value: string | undefined;
|
||||
constructor(value: string | undefined) {
|
||||
super("ak-search-select-select", { composed: true, bubbles: true });
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* class SearchSelectSelectMenuEvent
|
||||
*
|
||||
* Intended meaning: the user selected an item from the menu, either by clicking on it with the
|
||||
* mouse, or selecting it with the keyboard controls and pressing Enter or Space. This is
|
||||
* intercepted an interpreted internally, usually resulting in a throw of SearchSelectSelectEvent.
|
||||
* They have to be distinct to avoid an infinite event loop.
|
||||
*/
|
||||
export class SearchSelectSelectMenuEvent extends Event {
|
||||
value: string | undefined;
|
||||
constructor(value: string | undefined) {
|
||||
super("ak-search-select-select-menu", { composed: true, bubbles: true });
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* class SearchSelectCloseEvent
|
||||
*
|
||||
* Intended meaning: the user requested that the menu dropdown close. Usually triggered by pressing
|
||||
* the Escape key.
|
||||
*/
|
||||
export class SearchSelectCloseEvent extends Event {
|
||||
constructor() {
|
||||
super("ak-search-select-close", { composed: true, bubbles: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* class SearchSelectInputEvent
|
||||
*
|
||||
* Intended meaning: the user made a change to the content of the `<input>` field
|
||||
*/
|
||||
export class SearchSelectInputEvent extends Event {
|
||||
value: string | undefined;
|
||||
constructor(value: string | undefined) {
|
||||
super("ak-search-select-input", { composed: true, bubbles: true });
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"ak-search-select-select-menu": SearchSelectSelectMenuEvent;
|
||||
"ak-search-select-select": SearchSelectSelectEvent;
|
||||
"ak-search-select-input": SearchSelectInputEvent;
|
||||
"ak-search-select-close": SearchSelectCloseEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { autoUpdate, computePosition, flip, hide } from "@floating-ui/dom";
|
||||
|
||||
import { LitElement, html, nothing, render } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import { KeyboardControllerCloseEvent } from "./SearchKeyboardControllerEvents.js";
|
||||
import "./ak-search-select-menu.js";
|
||||
import { type SearchSelectMenu } from "./ak-search-select-menu.js";
|
||||
import type { SearchOptions } from "./types.js";
|
||||
|
||||
/**
|
||||
* An intermediate class to handle the menu and its position.
|
||||
*
|
||||
* It has no rendering of its own, and mostly is just a pass-through for options to the menu.
|
||||
* DOTADIW: it tracks the top-of-DOM object into which we render our menu, guaranteeing that it
|
||||
* appears above everything else, and operates the positioning control for it.
|
||||
*
|
||||
* - @fires ak-search-select-close - Fired (by the keyboard controller) when the tethered end loses
|
||||
* focus. Clients can do with this information as they wish.
|
||||
*/
|
||||
|
||||
@customElement("ak-search-select-menu-position")
|
||||
export class SearchSelectMenuPosition extends LitElement {
|
||||
/**
|
||||
* The host to which all relevant events will be routed. Useful for managing floating / tethered
|
||||
* components.
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
host!: HTMLElement;
|
||||
|
||||
/**
|
||||
* The host element which will be our reference point for rendering.
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
anchor!: HTMLElement;
|
||||
|
||||
/**
|
||||
* Passthrough of the options that we'll be rendering.
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
options: SearchOptions = [];
|
||||
|
||||
/**
|
||||
* Passthrough of the current value
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* If undefined, there will be no empty option shown
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
emptyOption?: string;
|
||||
|
||||
/**
|
||||
* Whether or not the menu is visible
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
/**
|
||||
* The name; used mostly for the management layer.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The tether object.
|
||||
*/
|
||||
dropdownContainer!: HTMLDivElement;
|
||||
public cleanup?: () => void;
|
||||
|
||||
connected = false;
|
||||
|
||||
/**
|
||||
*Communicates forward with the menu to detect when the tether has lost focus
|
||||
*/
|
||||
menuRef: Ref<SearchSelectMenu> = createRef();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.dropdownContainer.dataset["managedBy"] = "ak-search-select";
|
||||
if (this.name) {
|
||||
this.dropdownContainer.dataset["managedFor"] = this.name;
|
||||
}
|
||||
document.body.append(this.dropdownContainer);
|
||||
if (!this.host) {
|
||||
throw new Error("Tether entrance initialized incorrectly: missing host");
|
||||
}
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.connected = false;
|
||||
this.dropdownContainer?.remove();
|
||||
this.cleanup?.();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
setPosition() {
|
||||
if (!(this.anchor && this.dropdownContainer)) {
|
||||
throw new Error("Tether initialized incorrectly: missing anchor or tether destination");
|
||||
}
|
||||
|
||||
this.cleanup = autoUpdate(this.anchor, this.dropdownContainer, async () => {
|
||||
const { x, y } = await computePosition(this.anchor, this.dropdownContainer, {
|
||||
placement: "bottom-start",
|
||||
strategy: "fixed",
|
||||
middleware: [flip(), hide()],
|
||||
});
|
||||
|
||||
Object.assign(this.dropdownContainer.style, {
|
||||
"position": "fixed",
|
||||
"z-index": "9999",
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"transform": `translate(${x}px, ${y}px)`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updated() {
|
||||
if (this.anchor && this.dropdownContainer && !this.hidden) {
|
||||
this.setPosition();
|
||||
}
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
return (
|
||||
this.menuRef.value &&
|
||||
(this.menuRef.value === document.activeElement ||
|
||||
this.menuRef.value.renderRoot.contains(document.activeElement))
|
||||
);
|
||||
}
|
||||
|
||||
onFocusOut() {
|
||||
this.dispatchEvent(new KeyboardControllerCloseEvent());
|
||||
}
|
||||
|
||||
render() {
|
||||
// The 'hidden' attribute is a little weird and the current Typescript definition for
|
||||
// it is incompatible with actual implementations, so we drill `open` all the way down,
|
||||
// but we set the hidden attribute here, and on the actual menu use CSS and the
|
||||
// the attribute's presence to hide/show as needed.
|
||||
render(
|
||||
html`<ak-search-select-menu
|
||||
.options=${this.options}
|
||||
value=${ifDefined(this.value)}
|
||||
.host=${this.host}
|
||||
.emptyOption=${this.emptyOption}
|
||||
@focusout=${this.onFocusOut}
|
||||
?open=${this.open}
|
||||
?hidden=${!this.open}
|
||||
${ref(this.menuRef)}
|
||||
></ak-search-select-menu>`,
|
||||
this.dropdownContainer,
|
||||
);
|
||||
// This is a dummy object that just has to exist to be the communications channel between
|
||||
// the tethered object and its anchor.
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-search-select-menu-position": SearchSelectMenuPosition;
|
||||
}
|
||||
}
|
||||
192
web/src/elements/forms/SearchSelect/ak-search-select-menu.ts
Normal file
192
web/src/elements/forms/SearchSelect/ak-search-select-menu.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
|
||||
import { PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
|
||||
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { AkKeyboardController } from "./SearchKeyboardController.js";
|
||||
import {
|
||||
KeyboardControllerCloseEvent,
|
||||
KeyboardControllerSelectEvent,
|
||||
} from "./SearchKeyboardControllerEvents.js";
|
||||
import { SearchSelectCloseEvent, SearchSelectSelectMenuEvent } from "./SearchSelectEvents.js";
|
||||
import type { GroupedOptions, SearchGroup, SearchOptions, SearchTuple } from "./types.js";
|
||||
|
||||
/**
|
||||
* @class SearchSelectMenu
|
||||
* @element ak-search-select-menu
|
||||
*
|
||||
* The actual renderer of our components. Intended to be positioned and controlled automatically
|
||||
* from the outside.
|
||||
*
|
||||
* @fires ak-search-select-select - An element has been selected. Contains the `value` of the
|
||||
* selected item.
|
||||
*
|
||||
* @fires ak-search-select-close - The user has triggered the `close` event. Clients can do with this
|
||||
* as they wish.
|
||||
*/
|
||||
|
||||
@customElement("ak-search-select-menu")
|
||||
export class SearchSelectMenu extends AKElement {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFDropdown,
|
||||
PFSelect,
|
||||
css`
|
||||
:host {
|
||||
overflow: visible;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-c-dropdown__menu {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The host to which all relevant events will be routed. Useful for managing floating / tethered
|
||||
* components.
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
host!: HTMLElement;
|
||||
|
||||
/**
|
||||
* See the search options type, described in the `./types` file, for the relevant types.
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
options: SearchOptions = [];
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@property()
|
||||
emptyOption?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
private keyboardController: AkKeyboardController;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.keyboardController = new AkKeyboardController(this);
|
||||
this.addEventListener("ak-keyboard-controller-select", this.onKeySelect);
|
||||
this.addEventListener("ak-keyboard-controller-close", this.onKeyClose);
|
||||
}
|
||||
|
||||
// Handles the "easy mode" of just passing an array of tuples.
|
||||
fixedOptions(): GroupedOptions {
|
||||
return Array.isArray(this.options)
|
||||
? { grouped: false, options: this.options }
|
||||
: this.options;
|
||||
}
|
||||
|
||||
@bound
|
||||
onClick(event: Event, value: string) {
|
||||
event.stopPropagation();
|
||||
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(value));
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@bound
|
||||
onEmptyClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(undefined));
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
@bound
|
||||
onKeySelect(event: KeyboardControllerSelectEvent) {
|
||||
event.stopPropagation();
|
||||
this.value = event.value;
|
||||
this.host.dispatchEvent(new SearchSelectSelectMenuEvent(this.value));
|
||||
}
|
||||
|
||||
@bound
|
||||
onKeyClose(event: KeyboardControllerCloseEvent) {
|
||||
event.stopPropagation();
|
||||
this.host.dispatchEvent(new SearchSelectCloseEvent());
|
||||
}
|
||||
|
||||
updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("open") && this.open) {
|
||||
this.keyboardController.hostVisible();
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyMenuItem() {
|
||||
return html`<li>
|
||||
<button class="pf-c-dropdown__menu-item" role="option" @click=${this.onEmptyClick}>
|
||||
${this.emptyOption}
|
||||
</button>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
renderMenuItems(options: SearchTuple[]) {
|
||||
return options.map(
|
||||
([value, label, desc]: SearchTuple) => html`
|
||||
<li>
|
||||
<button
|
||||
class="pf-c-dropdown__menu-item pf-m-description ak-select-item"
|
||||
role="option"
|
||||
value=${value}
|
||||
@click=${(ev: Event) => {
|
||||
this.onClick(ev, value);
|
||||
}}
|
||||
@keypress=${() => {
|
||||
/* noop */
|
||||
}}
|
||||
>
|
||||
<div class="pf-c-dropdown__menu-item-main">${label}</div>
|
||||
${desc
|
||||
? html`<div class="pf-c-dropdown__menu-item-description">${desc}</div>`
|
||||
: nothing}
|
||||
</button>
|
||||
</li>
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
renderMenuGroups(options: SearchGroup[]) {
|
||||
return options.map(
|
||||
({ name, options }) => html`
|
||||
<section class="pf-c-dropdown__group">
|
||||
<h1 class="pf-c-dropdown__group-title">${name}</h1>
|
||||
<ul>
|
||||
${this.renderMenuItems(options)}
|
||||
</ul>
|
||||
</section>
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.fixedOptions();
|
||||
return html`<div class="pf-c-dropdown pf-m-expanded">
|
||||
<ul class="pf-c-dropdown__menu pf-m-static" role="listbox" tabindex="0">
|
||||
${this.emptyOption !== undefined ? this.renderEmptyMenuItem() : nothing}
|
||||
${options.grouped
|
||||
? this.renderMenuGroups(options.options)
|
||||
: this.renderMenuItems(options.options)}
|
||||
</ul>
|
||||
</div> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-search-select-menu": SearchSelectMenu;
|
||||
}
|
||||
}
|
||||
286
web/src/elements/forms/SearchSelect/ak-search-select-view.ts
Normal file
286
web/src/elements/forms/SearchSelect/ak-search-select-view.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-menu-position.js";
|
||||
import type { SearchSelectMenuPosition } from "@goauthentik/elements/forms/SearchSelect/ak-search-select-menu-position.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
SearchSelectCloseEvent,
|
||||
SearchSelectInputEvent,
|
||||
SearchSelectSelectEvent,
|
||||
SearchSelectSelectMenuEvent,
|
||||
} from "./SearchSelectEvents.js";
|
||||
import type { SearchOptions, SearchTuple } from "./types.js";
|
||||
|
||||
/**
|
||||
* @class SearchSelectView
|
||||
* @element ak-search-select-view
|
||||
*
|
||||
* Main component of ak-search-select, renders the <input> object and controls interaction with the
|
||||
* portaled menu list.
|
||||
*
|
||||
* @fires ak-search-select-input - When the user selects an item from the list. A derivative Event
|
||||
* with the `value` as its payload.
|
||||
*
|
||||
* Note that this is more on the HTML / Web Component side of the operational line: the keys which
|
||||
* represent the values we pass back to clients are always strings here. This component is strictly
|
||||
* for *rendering* and *interacting* with the items as the user sees them. If the host client is
|
||||
* not using strings for the values it ultimately keeps inside, it must map them forward to the
|
||||
* string-based keys we use here (along with the label and description), and map them *back* to
|
||||
* the object that key references when extracting the value for use.
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-search-select-view")
|
||||
export class SearchSelectView extends AKElement {
|
||||
/**
|
||||
* The options collection. The simplest variant is just [key, label, optional<description>]. See
|
||||
* the `./types.ts` file for variants and how to use them.
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
options: SearchOptions = [];
|
||||
|
||||
/**
|
||||
* The current value. Must be one of the keys in the options group above.
|
||||
*
|
||||
* @prop
|
||||
*/
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* If set to true, this object MAY return undefined in no value is passed in and none is set
|
||||
* during interaction.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
blankable = false;
|
||||
|
||||
/**
|
||||
* The name of the input, for forms
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Whether or not the portal is open
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
/**
|
||||
* The textual placeholder for the search's <input> object, if currently empty. Used as the
|
||||
* native <input> object's `placeholder` field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
placeholder: string = msg("Select an object.");
|
||||
|
||||
/**
|
||||
* A textual string representing "The user has affirmed they want to leave the selection blank."
|
||||
* Only used if `blankable` above is true.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
emptyOption = "---------";
|
||||
|
||||
// Handle the behavior of the drop-down when the :host scrolls off the page.
|
||||
scrollHandler?: () => void;
|
||||
observer: IntersectionObserver;
|
||||
|
||||
@state()
|
||||
displayValue = "";
|
||||
/**
|
||||
* Permanent identify for the input object, so the floating portal can find where to anchor
|
||||
* itself.
|
||||
*/
|
||||
inputRef: Ref<HTMLInputElement> = createRef();
|
||||
|
||||
/**
|
||||
* Permanent identity with the portal so focus events can be checked.
|
||||
*/
|
||||
menuRef: Ref<SearchSelectMenuPosition> = createRef();
|
||||
|
||||
/**
|
||||
* Maps a value from the portal to labels to be put into the <input> field>
|
||||
*/
|
||||
optionsMap: Map<string, string> = new Map();
|
||||
|
||||
static get styles() {
|
||||
return [PFBase, PFForm, PFFormControl, PFSelect];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.observer = new IntersectionObserver(() => {
|
||||
this.open = false;
|
||||
});
|
||||
this.observer.observe(this);
|
||||
|
||||
/* These can't be attached with the `@` syntax because they're not passed through to the
|
||||
* menu; the positioner is in the way, and it deliberately renders objects *outside* of the
|
||||
* path from `document` to this object. That's why we pass the positioner (and its target)
|
||||
* the `this` (host) object; so they can send messages to this object despite being outside
|
||||
* the event's bubble path.
|
||||
*/
|
||||
this.addEventListener("ak-search-select-select-menu", this.onSelect);
|
||||
this.addEventListener("ak-search-select-close", this.onClose);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.observer.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
onOpenEvent(event: Event) {
|
||||
this.open = true;
|
||||
if (
|
||||
this.blankable &&
|
||||
this.value === this.emptyOption &&
|
||||
event.target &&
|
||||
event.target instanceof HTMLInputElement
|
||||
) {
|
||||
event.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
@bound
|
||||
onSelect(event: SearchSelectSelectMenuEvent) {
|
||||
this.open = false;
|
||||
this.value = event.value;
|
||||
this.displayValue = this.value ? (this.optionsMap.get(this.value) ?? this.value ?? "") : "";
|
||||
this.dispatchEvent(new SearchSelectSelectEvent(this.value));
|
||||
}
|
||||
|
||||
@bound
|
||||
onClose(event: SearchSelectCloseEvent) {
|
||||
event.stopPropagation();
|
||||
this.inputRef.value?.focus();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
@bound
|
||||
onFocus(event: FocusEvent) {
|
||||
this.onOpenEvent(event);
|
||||
}
|
||||
|
||||
@bound
|
||||
onClick(event: Event) {
|
||||
this.onOpenEvent(event);
|
||||
}
|
||||
|
||||
@bound
|
||||
onInput(_event: InputEvent) {
|
||||
this.value = this.inputRef?.value?.value ?? "";
|
||||
this.displayValue = this.value ? (this.optionsMap.get(this.value) ?? this.value ?? "") : "";
|
||||
this.dispatchEvent(new SearchSelectInputEvent(this.value));
|
||||
}
|
||||
|
||||
@bound
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
@bound
|
||||
onFocusOut(event: FocusEvent) {
|
||||
event.stopPropagation();
|
||||
window.setTimeout(() => {
|
||||
if (!this.menuRef.value?.hasFocus()) {
|
||||
this.open = false;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
willUpdate(changed: PropertyValues<this>) {
|
||||
if (changed.has("options")) {
|
||||
this.optionsMap = optionsToOptionsMap(this.options);
|
||||
}
|
||||
if (changed.has("value")) {
|
||||
this.displayValue = this.value
|
||||
? (this.optionsMap.get(this.value) ?? this.value ?? "")
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
updated() {
|
||||
if (!(this.inputRef?.value && this.inputRef?.value?.value === this.displayValue)) {
|
||||
this.inputRef.value && (this.inputRef.value.value = this.displayValue);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div class="pf-c-select">
|
||||
<div class="pf-c-select__toggle pf-m-typeahead">
|
||||
<div class="pf-c-select__toggle-wrapper">
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="pf-c-form-control pf-c-select__toggle-typeahead"
|
||||
type="text"
|
||||
${ref(this.inputRef)}
|
||||
placeholder=${this.placeholder}
|
||||
spellcheck="false"
|
||||
@input=${this.onInput}
|
||||
@focus=${this.onFocus}
|
||||
@click=${this.onClick}
|
||||
@keydown=${this.onKeydown}
|
||||
@focusout=${this.onFocusOut}
|
||||
value=${this.displayValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ak-search-select-menu-position
|
||||
name=${ifDefined(this.name)}
|
||||
.options=${this.options}
|
||||
value=${ifDefined(this.value)}
|
||||
.host=${this}
|
||||
.anchor=${this.inputRef.value}
|
||||
.emptyOption=${(this.blankable && this.emptyOption) || undefined}
|
||||
${ref(this.menuRef)}
|
||||
?open=${this.open}
|
||||
></ak-search-select-menu-position> `;
|
||||
}
|
||||
}
|
||||
|
||||
type Pair = [string, string];
|
||||
const justThePair = ([key, label]: SearchTuple): Pair => [key, label];
|
||||
|
||||
function optionsToOptionsMap(options: SearchOptions): Map<string, string> {
|
||||
const pairs: Pair[] = Array.isArray(options)
|
||||
? options.map(justThePair)
|
||||
: options.grouped
|
||||
? options.options.reduce(
|
||||
(acc: Pair[], { options }): Pair[] => [...acc, ...options.map(justThePair)],
|
||||
[] as Pair[],
|
||||
)
|
||||
: options.options.map(justThePair);
|
||||
return new Map(pairs);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-search-select-view": SearchSelectView;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,31 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { APIErrorTypes, parseAPIError } from "@goauthentik/common/errors";
|
||||
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { TemplateResult, html, render } from "lit";
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { ResponseError } from "@goauthentik/api";
|
||||
|
||||
import { SearchSelectInputEvent, SearchSelectSelectEvent } from "./SearchSelectEvents.js";
|
||||
import "./ak-search-select-view.js";
|
||||
import type { GroupedOptions, SearchGroup, SearchTuple } from "./types.js";
|
||||
|
||||
type Group<T> = [string, T[]];
|
||||
|
||||
@customElement("ak-search-select")
|
||||
export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
|
||||
static get styles() {
|
||||
return [PFBase];
|
||||
}
|
||||
|
||||
// A function which takes the query state object (accepting that it may be empty) and returns a
|
||||
// new collection of objects.
|
||||
@property({ attribute: false })
|
||||
@@ -75,14 +78,10 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
@property({ attribute: false })
|
||||
selectedObject?: T;
|
||||
|
||||
// Not used in this object. No known purpose.
|
||||
// Used to inform the form of the name of the object
|
||||
@property()
|
||||
name?: string;
|
||||
|
||||
// Whether or not the dropdown component is visible.
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
// The textual placeholder for the search's <input> object, if currently empty. Used as the
|
||||
// native <input> object's `placeholder` field.
|
||||
@property()
|
||||
@@ -93,46 +92,14 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
@property()
|
||||
emptyOption = "---------";
|
||||
|
||||
// Handle the behavior of the drop-down when the :host scrolls off the page.
|
||||
scrollHandler?: () => void;
|
||||
observer: IntersectionObserver;
|
||||
|
||||
// Handle communication between the :host and the portal
|
||||
dropdownUID: string;
|
||||
dropdownContainer: HTMLDivElement;
|
||||
|
||||
isFetchingData = false;
|
||||
|
||||
@state()
|
||||
error?: APIErrorTypes;
|
||||
|
||||
static get styles() {
|
||||
return [PFBase, PFForm, PFFormControl, PFSelect];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!document.adoptedStyleSheets.includes(PFDropdown)) {
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
ensureCSSStyleSheet(PFDropdown),
|
||||
];
|
||||
}
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.observer = new IntersectionObserver(() => {
|
||||
this.open = false;
|
||||
this.shadowRoot
|
||||
?.querySelectorAll<HTMLInputElement>(
|
||||
".pf-c-form-control.pf-c-select__toggle-typeahead",
|
||||
)
|
||||
.forEach((input) => {
|
||||
input.blur();
|
||||
});
|
||||
});
|
||||
this.observer.observe(this);
|
||||
this.dropdownUID = `dropdown-${randomString(10, ascii_letters + digits)}`;
|
||||
this.onMenuItemClick = this.onMenuItemClick.bind(this);
|
||||
this.renderWithMenuGroupTitle = this.renderWithMenuGroupTitle.bind(this);
|
||||
this.dataset.akControl = "true";
|
||||
}
|
||||
|
||||
toForm(): unknown {
|
||||
@@ -142,16 +109,16 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
return this.value(this.selectedObject) || "";
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
this.updateData();
|
||||
json() {
|
||||
return this.toForm();
|
||||
}
|
||||
|
||||
updateData(): void {
|
||||
updateData() {
|
||||
if (this.isFetchingData) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingData = true;
|
||||
this.fetchObjects(this.query)
|
||||
return this.fetchObjects(this.query)
|
||||
.then((objects) => {
|
||||
objects.forEach((obj) => {
|
||||
if (this.selected && this.selected(obj, objects || [])) {
|
||||
@@ -173,230 +140,97 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.dropdownContainer.dataset["managedBy"] = "ak-search-select";
|
||||
if (this.name) {
|
||||
this.dropdownContainer.dataset["managedFor"] = this.name;
|
||||
}
|
||||
document.body.append(this.dropdownContainer);
|
||||
this.updateData();
|
||||
this.addEventListener(EVENT_REFRESH, this.updateData);
|
||||
this.scrollHandler = () => {
|
||||
this.requestUpdate();
|
||||
};
|
||||
window.addEventListener("scroll", this.scrollHandler);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(EVENT_REFRESH, this.updateData);
|
||||
if (this.scrollHandler) {
|
||||
window.removeEventListener("scroll", this.scrollHandler);
|
||||
}
|
||||
this.dropdownContainer.remove();
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
renderMenuItemWithDescription(obj: T, desc: TemplateResult, index: number) {
|
||||
return html`
|
||||
<li>
|
||||
<button
|
||||
class="pf-c-dropdown__menu-item pf-m-description"
|
||||
role="option"
|
||||
@click=${this.onMenuItemClick(obj)}
|
||||
tabindex=${index}
|
||||
>
|
||||
<div class="pf-c-dropdown__menu-item-main">${this.renderElement(obj)}</div>
|
||||
<div class="pf-c-dropdown__menu-item-description">${desc}</div>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
renderMenuItemWithoutDescription(obj: T, index: number) {
|
||||
return html`
|
||||
<li>
|
||||
<button
|
||||
class="pf-c-dropdown__menu-item"
|
||||
role="option"
|
||||
@click=${this.onMenuItemClick(obj)}
|
||||
tabindex=${index}
|
||||
>
|
||||
${this.renderElement(obj)}
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
renderEmptyMenuItem() {
|
||||
return html`<li>
|
||||
<button
|
||||
class="pf-c-dropdown__menu-item"
|
||||
role="option"
|
||||
@click=${this.onMenuItemClick(undefined)}
|
||||
tabindex="0"
|
||||
>
|
||||
${this.emptyOption}
|
||||
</button>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
onMenuItemClick(obj: T | undefined) {
|
||||
return () => {
|
||||
this.selectedObject = obj;
|
||||
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
|
||||
this.open = false;
|
||||
};
|
||||
}
|
||||
|
||||
renderMenuGroup(items: T[], tabIndexStart: number) {
|
||||
const renderedItems = items.map((obj, index) => {
|
||||
const desc = this.renderDescription ? this.renderDescription(obj) : null;
|
||||
const tabIndex = index + tabIndexStart;
|
||||
return desc
|
||||
? this.renderMenuItemWithDescription(obj, desc, tabIndex)
|
||||
: this.renderMenuItemWithoutDescription(obj, tabIndex);
|
||||
});
|
||||
return html`${renderedItems}`;
|
||||
}
|
||||
|
||||
renderWithMenuGroupTitle([group, items]: Group<T>, idx: number) {
|
||||
return html`
|
||||
<section class="pf-c-dropdown__group">
|
||||
<h1 class="pf-c-dropdown__group-title">${group}</h1>
|
||||
<ul>
|
||||
${this.renderMenuGroup(items, idx)}
|
||||
</ul>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
get groupedItems(): [boolean, Group<T>[]] {
|
||||
const items = this.groupBy(this.objects || []);
|
||||
if (items.length === 0) {
|
||||
return [false, [["", []]]];
|
||||
}
|
||||
if (items.length === 1 && (items[0].length < 1 || items[0][0] === "")) {
|
||||
return [false, items];
|
||||
}
|
||||
return [true, items];
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a little bit hacky. Because we mainly want to use this field in modal-based forms,
|
||||
* rendering this menu inline makes the menu not overlay over top of the modal, and cause
|
||||
* the modal to scroll.
|
||||
* Hence, we render the menu into the document root, hide it when this menu isn't open
|
||||
* and remove it on disconnect
|
||||
* Also to move it to the correct position we're getting this elements's position and use that
|
||||
* to position the menu
|
||||
* The other downside this has is that, since we're rendering outside of a shadow root,
|
||||
* the pf-c-dropdown CSS needs to be loaded on the body.
|
||||
*/
|
||||
|
||||
renderMenu(): void {
|
||||
if (!this.objects) {
|
||||
onSearch(event: SearchSelectInputEvent) {
|
||||
if (event.value === undefined) {
|
||||
this.selectedObject = undefined;
|
||||
return;
|
||||
}
|
||||
const [shouldRenderGroups, groupedItems] = this.groupedItems;
|
||||
|
||||
const pos = this.getBoundingClientRect();
|
||||
const position = {
|
||||
"position": "fixed",
|
||||
"inset": "0px auto auto 0px",
|
||||
"z-index": "9999",
|
||||
"transform": `translate(${pos.x}px, ${pos.y + this.offsetHeight}px)`,
|
||||
"width": `${pos.width}px`,
|
||||
...(this.open ? {} : { visibility: "hidden" }),
|
||||
};
|
||||
|
||||
render(
|
||||
html`<div style=${styleMap(position)} class="pf-c-dropdown pf-m-expanded">
|
||||
<ul
|
||||
class="pf-c-dropdown__menu pf-m-static"
|
||||
role="listbox"
|
||||
style="max-height:50vh;overflow-y:auto;"
|
||||
id=${this.dropdownUID}
|
||||
tabindex="0"
|
||||
>
|
||||
${this.blankable ? this.renderEmptyMenuItem() : html``}
|
||||
${shouldRenderGroups
|
||||
? html`${groupedItems.map(this.renderWithMenuGroupTitle)}`
|
||||
: html`${this.renderMenuGroup(groupedItems[0][1], 0)}`}
|
||||
</ul>
|
||||
</div>`,
|
||||
this.dropdownContainer,
|
||||
{ host: this },
|
||||
);
|
||||
this.query = event.value;
|
||||
this.updateData()?.then(() => {
|
||||
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
|
||||
});
|
||||
}
|
||||
|
||||
get renderedValue() {
|
||||
onSelect(event: SearchSelectSelectEvent) {
|
||||
if (event.value === undefined) {
|
||||
this.selectedObject = undefined;
|
||||
this.dispatchCustomEvent("ak-change", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const selected = (this.objects ?? []).find((obj) => `${this.value(obj)}` === event.value);
|
||||
if (!selected) {
|
||||
console.warn(
|
||||
`ak-search-select: No corresponding object found for value (${event.value}`,
|
||||
);
|
||||
}
|
||||
this.selectedObject = selected;
|
||||
this.dispatchCustomEvent("ak-change", { value: this.selectedObject });
|
||||
}
|
||||
|
||||
getGroupedItems(): GroupedOptions {
|
||||
const items = this.groupBy(this.objects || []);
|
||||
const makeSearchTuples = (items: T[]): SearchTuple[] =>
|
||||
items.map((item) => [
|
||||
`${this.value(item)}`,
|
||||
this.renderElement(item),
|
||||
this.renderDescription ? this.renderDescription(item) : undefined,
|
||||
]);
|
||||
|
||||
const makeSearchGroups = (items: Group<T>[]): SearchGroup[] =>
|
||||
items.map((group) => ({
|
||||
name: group[0],
|
||||
options: makeSearchTuples(group[1]),
|
||||
}));
|
||||
|
||||
if (items.length === 0) {
|
||||
return { grouped: false, options: [] };
|
||||
}
|
||||
|
||||
if (items.length === 1 && (items[0].length < 1 || items[0][0] === "")) {
|
||||
return {
|
||||
grouped: false,
|
||||
options: makeSearchTuples(items[0][1]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
grouped: true,
|
||||
options: makeSearchGroups(items),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.error) {
|
||||
return msg(str`Failed to fetch objects: ${this.error.detail}`);
|
||||
return html`<em>${msg("Failed to fetch objects: ")} ${this.error.detail}</em>`;
|
||||
}
|
||||
|
||||
if (!this.objects) {
|
||||
return msg("Loading...");
|
||||
return html`${msg("Loading...")}`;
|
||||
}
|
||||
if (this.selectedObject) {
|
||||
return this.renderElement(this.selectedObject);
|
||||
}
|
||||
if (this.blankable) {
|
||||
return this.emptyOption;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
this.renderMenu();
|
||||
const options = this.getGroupedItems();
|
||||
const value = this.selectedObject ? `${this.value(this.selectedObject) ?? ""}` : undefined;
|
||||
|
||||
const onFocus = (ev: FocusEvent) => {
|
||||
this.open = true;
|
||||
this.renderMenu();
|
||||
if (this.blankable && this.renderedValue === this.emptyOption) {
|
||||
if (ev.target && ev.target instanceof HTMLInputElement) {
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = (ev: InputEvent) => {
|
||||
this.query = (ev.target as HTMLInputElement).value;
|
||||
this.updateData();
|
||||
};
|
||||
|
||||
const onBlur = (ev: FocusEvent) => {
|
||||
// For Safari, we get the <ul> element itself here when clicking on one of
|
||||
// it's buttons, as the container has tabindex set
|
||||
if (ev.relatedTarget && (ev.relatedTarget as HTMLElement).id === this.dropdownUID) {
|
||||
return;
|
||||
}
|
||||
// Check if we're losing focus to one of our dropdown items, and if such don't blur
|
||||
if (ev.relatedTarget instanceof HTMLButtonElement) {
|
||||
const parentMenu = ev.relatedTarget.closest("ul.pf-c-dropdown__menu.pf-m-static");
|
||||
if (parentMenu && parentMenu.id === this.dropdownUID) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.open = false;
|
||||
this.renderMenu();
|
||||
};
|
||||
|
||||
return html`<div class="pf-c-select">
|
||||
<div class="pf-c-select__toggle pf-m-typeahead">
|
||||
<div class="pf-c-select__toggle-wrapper">
|
||||
<input
|
||||
class="pf-c-form-control pf-c-select__toggle-typeahead"
|
||||
type="text"
|
||||
placeholder=${this.placeholder}
|
||||
spellcheck="false"
|
||||
@input=${onInput}
|
||||
@focus=${onFocus}
|
||||
@blur=${onBlur}
|
||||
.value=${this.renderedValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return html`<ak-search-select-view
|
||||
.options=${options}
|
||||
.value=${value}
|
||||
?blankable=${this.blankable}
|
||||
name=${ifDefined(this.name)}
|
||||
placeholder=${this.placeholder}
|
||||
emptyOption=${ifDefined(this.blankable ? this.emptyOption : undefined)}
|
||||
@ak-search-select-input=${this.onSearch}
|
||||
@ak-search-select-select=${this.onSelect}
|
||||
></ak-search-select-view> `;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { slug } from "github-slugger";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import { SearchSelectSelectMenuEvent } from "../SearchSelectEvents.js";
|
||||
import "../ak-search-select-menu.js";
|
||||
import { SearchSelectMenu } from "../ak-search-select-menu.js";
|
||||
import { groupedSampleData, sampleData } from "./sampleData.js";
|
||||
|
||||
const metadata: Meta<SearchSelectMenu> = {
|
||||
title: "Elements / Search Select / Tethered Menu",
|
||||
component: "ak-search-select-menu",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "The tethered panel containing the scrollable list of selectable items",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
options: {
|
||||
type: "string",
|
||||
description: "An array of [key, label, desc] pairs of what to show",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onClick = (event: SearchSelectSelectMenuEvent) => {
|
||||
const target = document.querySelector("#action-button-message-pad");
|
||||
target!.innerHTML = "";
|
||||
target!.append(
|
||||
new DOMParser().parseFromString(`<li>${event.value}</li>`, "text/xml").firstChild!,
|
||||
);
|
||||
};
|
||||
|
||||
const container = (testItem: TemplateResult) => {
|
||||
window.setTimeout(() => {
|
||||
const menu = document.getElementById("ak-search-select-menu");
|
||||
const container = document.getElementById("the-main-event");
|
||||
if (menu && container) {
|
||||
container.addEventListener("ak-search-select-select-menu", onClick);
|
||||
(menu as SearchSelectMenu).host = container;
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return html` <div
|
||||
style="background: #fff; padding: 2em; position: relative"
|
||||
id="the-main-event"
|
||||
>
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
#the-answer-block {
|
||||
padding-top: 3em;
|
||||
}
|
||||
</style>
|
||||
<ak-message-container></ak-message-container>
|
||||
${testItem}
|
||||
<div id="the-answer-block">
|
||||
<p>Messages received from the menu:</p>
|
||||
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
const goodForYouPairs = {
|
||||
grouped: false,
|
||||
options: sampleData.slice(0, 20).map(({ produce }) => [slug(produce), produce]),
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () =>
|
||||
container(
|
||||
html` <ak-search-select-menu
|
||||
id="ak-search-select-menu"
|
||||
style="top: 1em; left: 1em"
|
||||
.options=${goodForYouPairs}
|
||||
></ak-search-select-menu>`,
|
||||
),
|
||||
};
|
||||
|
||||
const longGoodForYouPairs = {
|
||||
grouped: false,
|
||||
options: sampleData.map(({ produce }) => [slug(produce), produce]),
|
||||
};
|
||||
|
||||
export const Scrolling: Story = {
|
||||
render: () =>
|
||||
container(
|
||||
html` <ak-search-select-menu
|
||||
id="ak-search-select-menu"
|
||||
style="top: 1em; left: 1em"
|
||||
.options=${longGoodForYouPairs}
|
||||
.host=${document}
|
||||
></ak-search-select-menu>`,
|
||||
),
|
||||
};
|
||||
|
||||
export const Grouped: Story = {
|
||||
render: () =>
|
||||
container(
|
||||
html` <ak-search-select-menu
|
||||
id="ak-search-select-menu"
|
||||
style="top: 1em; left: 1em"
|
||||
.options=${groupedSampleData}
|
||||
.host=${document}
|
||||
></ak-search-select-menu>`,
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-view.js";
|
||||
import { SearchSelectView } from "@goauthentik/elements/forms/SearchSelect/ak-search-select-view.js";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
import { slug } from "github-slugger";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import { groupedSampleData, sampleData } from "./sampleData.js";
|
||||
|
||||
const metadata: Meta<SearchSelectView> = {
|
||||
title: "Elements / Search Select / View Handler ",
|
||||
component: "ak-search-select-view",
|
||||
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>`;
|
||||
|
||||
const longGoodForYouPairs = {
|
||||
grouped: false,
|
||||
options: sampleData.map(({ produce }) => [slug(produce), produce]),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return container(
|
||||
html`<ak-search-select-view
|
||||
.options=${longGoodForYouPairs}
|
||||
blankable
|
||||
@ak-search-select-select=${displayChange}
|
||||
></ak-search-select-view>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const DescribedGroups = () => {
|
||||
return container(
|
||||
html`<ak-search-select-view
|
||||
.options=${groupedSampleData}
|
||||
blankable
|
||||
@ak-search-select-select=${displayChange}
|
||||
></ak-search-select-view>`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
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";
|
||||
|
||||
import { sampleData } from "./sampleData.js";
|
||||
|
||||
type Sample = { name: string; pk: string; season: string[] };
|
||||
|
||||
const samples = sampleData.map(({ produce, seasons }) => ({
|
||||
name: produce,
|
||||
pk: produce.replace(/\s+/, "").toLowerCase(),
|
||||
season: seasons,
|
||||
}));
|
||||
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 = "") => {
|
||||
if (query === "") {
|
||||
return Promise.resolve(samples);
|
||||
}
|
||||
const check = new RegExp(query);
|
||||
return Promise.resolve(samples.filter((s) => check.test(s.name)));
|
||||
};
|
||||
|
||||
const metadata: Meta<SearchSelect<Sample>> = {
|
||||
title: "Elements / Search Select / API Interface",
|
||||
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 = () =>
|
||||
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 SelectedAndBlankable = () => {
|
||||
return container(
|
||||
html`<ak-search-select
|
||||
blankable
|
||||
.fetchObjects=${getSamples}
|
||||
.renderElement=${(sample: Sample) => sample.name}
|
||||
.value=${(sample: Sample) => sample.pk}
|
||||
.selected=${(sample: Sample) => sample.pk === "herbs"}
|
||||
@ak-change=${displayChange}
|
||||
></ak-search-select>`,
|
||||
);
|
||||
};
|
||||
359
web/src/elements/forms/SearchSelect/stories/sampleData.ts
Normal file
359
web/src/elements/forms/SearchSelect/stories/sampleData.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { slug } from "github-slugger";
|
||||
|
||||
import type { TemplateResult } from "lit";
|
||||
|
||||
// The descriptions were generated by ChatGPT. Don't blame us.
|
||||
|
||||
export type ViewSample = {
|
||||
produce: string;
|
||||
seasons: string[];
|
||||
desc?: string;
|
||||
};
|
||||
|
||||
export const sampleData: ViewSample[] = [
|
||||
{
|
||||
produce: "Apples",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Apples are a sweet and crunchy fruit that can be eaten fresh or used in pies, juice, and ciders.",
|
||||
},
|
||||
{
|
||||
produce: "Apricots",
|
||||
seasons: ["Spring", "Summer"],
|
||||
desc: "Apricots are a sweet and tangy stone fruit with a velvety skin that's often orange-yellow in color",
|
||||
},
|
||||
{
|
||||
produce: "Asparagus",
|
||||
seasons: ["Spring"],
|
||||
desc: "Asparagus is a delicate and nutritious vegetable with a tender spear-like shape",
|
||||
},
|
||||
{
|
||||
produce: "Avocados",
|
||||
seasons: ["Spring", "Summer", "Winter"],
|
||||
desc: "Avocados are a nutritious fruit with a creamy texture and nutty flavor",
|
||||
},
|
||||
{
|
||||
produce: "Bananas",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Bananas are a type of curved, yellow fruit that grows on banana plants",
|
||||
},
|
||||
{
|
||||
produce: "Beets",
|
||||
seasons: ["Summer", "Fall", "Winter"],
|
||||
desc: "Beets are a sweet and earthy root vegetable that can be pickled, roasted, or boiled",
|
||||
},
|
||||
{
|
||||
produce: "Bell Peppers",
|
||||
seasons: ["Summer", "Fall"],
|
||||
desc: "Bell peppers are a sweet and crunchy type of pepper that can be green, red, yellow, or orange",
|
||||
},
|
||||
{
|
||||
produce: "Blackberries",
|
||||
seasons: ["Summer"],
|
||||
desc: "Blackberries are a type of fruit that are dark purple in color and have a sweet-tart taste",
|
||||
},
|
||||
{
|
||||
produce: "Blueberries",
|
||||
seasons: ["Summer"],
|
||||
desc: "Blueberries are small, round, and sweet-tart berries with a powdery coating and a burst of juicy flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Broccoli",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Broccoli is a green, cruciferous vegetable with a tree-like shape and a slightly bitter taste.",
|
||||
},
|
||||
{
|
||||
produce: "Brussels Sprouts",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Brussels sprouts are a cruciferous vegetable that is small, green, and formed like a tiny cabbage head, with a sweet and slightly bitter flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Cabbage",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Cabbage is a crunchy, sweet, and slightly bitter vegetable with a dense head of tightly packed leaves.",
|
||||
},
|
||||
{
|
||||
produce: "Cantaloupe",
|
||||
seasons: ["Summer"],
|
||||
desc: "Cantaloupe is a sweet and juicy melon with a netted or reticulated rind and yellow-orange flesh.",
|
||||
},
|
||||
{
|
||||
produce: "Carrots",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Carrots are a crunchy and sweet root vegetable commonly eaten raw or cooked in various dishes.",
|
||||
},
|
||||
{
|
||||
produce: "Cauliflower",
|
||||
seasons: ["Fall"],
|
||||
desc: "Cauliflower is a cruciferous vegetable with a white or pale yellow florets resembling tiny trees",
|
||||
},
|
||||
{
|
||||
produce: "Celery",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Celery is a crunchy, sweet-tasting vegetable with a mild flavor, often used in salads and as a snack.",
|
||||
},
|
||||
{
|
||||
produce: "Cherries",
|
||||
seasons: ["Summer"],
|
||||
desc: "Cherries are a sweet and juicy stone fruit that typically range in color from bright red to dark purple.",
|
||||
},
|
||||
{
|
||||
produce: "Collard Greens",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Collard greens are a type of leafy green vegetable with a slightly bitter and earthy flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Corn",
|
||||
seasons: ["Summer"],
|
||||
desc: "Corn is a sweet and savory grain that can be eaten fresh or used in various dishes, such as soups, salads, and baked goods.",
|
||||
},
|
||||
{
|
||||
produce: "Cranberries",
|
||||
seasons: ["Fall"],
|
||||
desc: "Cranberries are a type of small, tart-tasting fruit native to North America",
|
||||
},
|
||||
{
|
||||
produce: "Cucumbers",
|
||||
seasons: ["Summer"],
|
||||
desc: "Cucumbers are a long, green vegetable that is commonly consumed raw or pickled",
|
||||
},
|
||||
{
|
||||
produce: "Eggplant",
|
||||
seasons: ["Summer"],
|
||||
desc: "Eggplant is a purple vegetable with a spongy texture and a slightly bitter taste.",
|
||||
},
|
||||
{
|
||||
produce: "Garlic",
|
||||
seasons: ["Spring", "Summer", "Fall"],
|
||||
desc: "Garlic is a pungent and flavorful herb with a distinctive aroma and taste",
|
||||
},
|
||||
{
|
||||
produce: "Ginger",
|
||||
seasons: ["Fall"],
|
||||
desc: "Ginger is a spicy, sweet, and tangy root commonly used in Asian cuisine to add warmth and depth",
|
||||
},
|
||||
{
|
||||
produce: "Grapefruit",
|
||||
seasons: ["Winter"],
|
||||
desc: "Grapefruit is a tangy and sweet citrus fruit with a tart flavor profile and a slightly bitter aftertaste.",
|
||||
},
|
||||
{
|
||||
produce: "Grapes",
|
||||
seasons: ["Fall"],
|
||||
desc: "Grapes are a type of fruit that grow in clusters on vines and are often eaten fresh or used to make wine, jam, and juice.",
|
||||
},
|
||||
{
|
||||
produce: "Green Beans",
|
||||
seasons: ["Summer", "Fall"],
|
||||
desc: "Green beans are a type of long, thin, green vegetable that is commonly eaten as a side dish or used in various recipes.",
|
||||
},
|
||||
{
|
||||
produce: "Herbs",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Herbs are plant parts, such as leaves, stems, or flowers, used to add flavor or aroma",
|
||||
},
|
||||
{
|
||||
produce: "Honeydew Melon",
|
||||
seasons: ["Summer"],
|
||||
desc: "Honeydew melons are sweet and refreshing, with a smooth, pale green rind and juicy, creamy white flesh.",
|
||||
},
|
||||
{
|
||||
produce: "Kale",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Kale is a type of leafy green vegetable that is packed with nutrients and has a slightly bitter, earthy flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Kiwifruit",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Kiwifruit is a small, oval-shaped fruit with a fuzzy exterior and bright green or yellow flesh that tastes sweet and slightly tart.",
|
||||
},
|
||||
{
|
||||
produce: "Leeks",
|
||||
seasons: ["Winter"],
|
||||
desc: "Leeks are a type of vegetable that is similar to onions and garlic, but has a milder flavor and a more delicate texture.",
|
||||
},
|
||||
{
|
||||
produce: "Lemons",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Lemons are a sour and tangy citrus fruit with a bright yellow color and a strong, distinctive flavor used in cooking, cleaning, and as a natural remedy.",
|
||||
},
|
||||
{
|
||||
produce: "Lettuce",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Lettuce is a crisp and refreshing green leafy vegetable often used in salads.",
|
||||
},
|
||||
{
|
||||
produce: "Lima Beans",
|
||||
seasons: ["Summer"],
|
||||
desc: "Lima beans are a type of green legume with a mild flavor and soft, creamy texture.",
|
||||
},
|
||||
{
|
||||
produce: "Limes",
|
||||
seasons: ["Spring", "Summer", "Fall", "Winter"],
|
||||
desc: "Limes are small, citrus fruits with a sour taste and a bright green color.",
|
||||
},
|
||||
{
|
||||
produce: "Mangos",
|
||||
seasons: ["Summer", "Fall"],
|
||||
desc: "Mangos are sweet and creamy tropical fruits with a velvety texture",
|
||||
},
|
||||
{
|
||||
produce: "Mushrooms",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Mushrooms are a type of fungus that grow underground or on decaying organic matter",
|
||||
},
|
||||
{
|
||||
produce: "Okra",
|
||||
seasons: ["Summer"],
|
||||
desc: "Okra is a nutritious, green vegetable with a unique texture and flavor",
|
||||
},
|
||||
{
|
||||
produce: "Onions",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Onions are a type of vegetable characterized by their layered, bulbous structure and pungent flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Oranges",
|
||||
seasons: ["Winter"],
|
||||
desc: "Oranges are a sweet and juicy citrus fruit with a thick, easy-to-peel skin.",
|
||||
},
|
||||
{
|
||||
produce: "Parsnips",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Parsnips are a type of root vegetable that is sweet and nutty in flavor, with a texture similar to carrots.",
|
||||
},
|
||||
{
|
||||
produce: "Peaches",
|
||||
seasons: ["Summer"],
|
||||
desc: "Peaches are sweet and juicy stone fruits with a soft, velvety texture.",
|
||||
},
|
||||
{
|
||||
produce: "Pears",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Pears are a type of sweet and juicy fruit with a smooth, buttery texture and a mild flavor",
|
||||
},
|
||||
{
|
||||
produce: "Peas",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Peas are small, round, sweet-tasting legumes that grow on vines and are often eaten as a side dish or added to various recipes.",
|
||||
},
|
||||
{
|
||||
produce: "Pineapples",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Pineapples are a tropical fruit with tough, prickly skin and juicy, sweet flesh.",
|
||||
},
|
||||
{
|
||||
produce: "Plums",
|
||||
seasons: ["Summer"],
|
||||
desc: "Plums are a type of stone fruit characterized by their juicy sweetness and rough, dark skin.",
|
||||
},
|
||||
{
|
||||
produce: "Potatoes",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Potatoes are a starchy root vegetable that is often brown on the outside and white or yellow on the inside.",
|
||||
},
|
||||
{
|
||||
produce: "Pumpkin",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Pumpkin is a type of squash that is typically orange in color and is often used to make pies, soups, and other sweet or savory dishes.",
|
||||
},
|
||||
{
|
||||
produce: "Radishes",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Radishes are a pungent, crunchy and spicy root vegetable that can be eaten raw or cooked,",
|
||||
},
|
||||
{
|
||||
produce: "Raspberries",
|
||||
seasons: ["Summer", "Fall"],
|
||||
desc: "Raspberries are a type of sweet-tart fruit that grows on thorny bushes and is often eaten fresh or used in jams, preserves, and desserts.",
|
||||
},
|
||||
{
|
||||
produce: "Rhubarb",
|
||||
seasons: ["Spring"],
|
||||
desc: "Rhubarb is a perennial vegetable with long, tart stalks that are often used in pies and preserves",
|
||||
},
|
||||
{
|
||||
produce: "Rutabagas",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Rutabagas are a type of root vegetable that is similar to a cross between a cabbage and a turnip",
|
||||
},
|
||||
{
|
||||
produce: "Spinach",
|
||||
seasons: ["Spring", "Fall"],
|
||||
desc: "Spinach is a nutritious leafy green vegetable that is rich in iron and vitamins A, C, and K.",
|
||||
},
|
||||
{
|
||||
produce: "Strawberries",
|
||||
seasons: ["Spring", "Summer"],
|
||||
desc: "Sweet and juicy, strawberries are a popular type of fruit that grow on low-lying plants with sweet-tasting seeds.",
|
||||
},
|
||||
{
|
||||
produce: "Summer Squash",
|
||||
seasons: ["Summer"],
|
||||
desc: "Summer squash is a type of warm-season vegetable that includes varieties like zucchini, yellow crookneck, and straightneck",
|
||||
},
|
||||
{
|
||||
produce: "Sweet Potatoes",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Sweet potatoes are a type of root vegetable with a sweet and nutty flavor, often orange in color",
|
||||
},
|
||||
{
|
||||
produce: "Swiss Chard",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Swiss Chard is a leafy green vegetable with a slightly bitter taste and a vibrant red or gold stem",
|
||||
},
|
||||
{
|
||||
produce: "Tomatillos",
|
||||
seasons: ["Summer"],
|
||||
desc: "Tomatillos are a type of fruit that is similar to tomatoes, but with a papery husk and a more tart, slightly sweet flavor.",
|
||||
},
|
||||
{
|
||||
produce: "Tomatoes",
|
||||
seasons: ["Summer"],
|
||||
desc: "Tomatoes are a juicy, sweet, and tangy fruit that is commonly used in salads, sandwiches, and as a topping for various dishes.",
|
||||
},
|
||||
{
|
||||
produce: "Turnips",
|
||||
seasons: ["Spring", "Fall", "Winter"],
|
||||
desc: "Turnips are a root vegetable with a sweet and peppery flavor, often used in soups, stews, and salads.",
|
||||
},
|
||||
{
|
||||
produce: "Watermelon",
|
||||
seasons: ["Summer"],
|
||||
desc: "Watermelon is a juicy and refreshing sweet fruit with a green rind and pink or yellow flesh.",
|
||||
},
|
||||
{
|
||||
produce: "Winter Squash",
|
||||
seasons: ["Fall", "Winter"],
|
||||
desc: "Winter squash is a type of starchy vegetable that is harvested in the fall and has a hard, dry rind that can be stored for several months.",
|
||||
},
|
||||
{
|
||||
produce: "Zucchini",
|
||||
seasons: ["Summer"],
|
||||
desc: "Zucchini is a popular summer squash that is often green or yellow in color and has a mild, slightly sweet flavor.",
|
||||
},
|
||||
];
|
||||
|
||||
type Seasoned = [string, string, string | TemplateResult];
|
||||
|
||||
const reseason = (acc: Seasoned[], { produce, seasons, desc }: ViewSample): Seasoned[] => [
|
||||
...acc,
|
||||
...seasons.map((s) => [s, produce, desc] as Seasoned),
|
||||
];
|
||||
|
||||
export const groupedSampleData = (() => {
|
||||
const seasoned: Seasoned[] = sampleData.reduce(reseason, [] as Seasoned[]);
|
||||
const grouped = Object.groupBy(seasoned, ([season]) => season);
|
||||
const ungrouped = ([_season, label, desc]: Seasoned) => [slug(label), label, desc];
|
||||
|
||||
if (grouped === undefined) {
|
||||
throw new Error("Not possible with existing data.");
|
||||
}
|
||||
|
||||
return {
|
||||
grouped: true,
|
||||
options: ["Spring", "Summer", "Fall", "Winter"].map((season) => ({
|
||||
name: season,
|
||||
options: grouped[season]?.map(ungrouped) ?? [],
|
||||
})),
|
||||
};
|
||||
})();
|
||||
66
web/src/elements/forms/SearchSelect/types.ts
Normal file
66
web/src/elements/forms/SearchSelect/types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
|
||||
/**
|
||||
* A search tuple consists of a [key, label, description]
|
||||
* The description is optional. The key must always be a string.
|
||||
*
|
||||
*/
|
||||
export type SearchTuple = [
|
||||
key: string,
|
||||
label: string,
|
||||
description: undefined | string | TemplateResult,
|
||||
];
|
||||
|
||||
/**
|
||||
* A search list without groups will always just consist of an array of SearchTuples and the
|
||||
* `grouped: false` flag. Note that it *is* possible to pass to any of the rendering components an
|
||||
* array of SearchTuples; they will be automatically mapped to a SearchFlat object.
|
||||
*
|
||||
*/
|
||||
export type SearchFlat = {
|
||||
grouped: false;
|
||||
options: SearchTuple[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A search group consists of a group name and a collection of SearchTuples.
|
||||
*
|
||||
*/
|
||||
export type SearchGroup = { name: string; options: SearchTuple[] };
|
||||
|
||||
/**
|
||||
* A grouped search is an array of SearchGroups, of course!
|
||||
*
|
||||
*/
|
||||
export type SearchGrouped = {
|
||||
grouped: true;
|
||||
options: SearchGroup[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Internally, we only work with these two, but we have the `SearchOptions` variant
|
||||
* below to support the case where you just want to pass in an array of SearchTuples.
|
||||
*
|
||||
*/
|
||||
export type GroupedOptions = SearchGrouped | SearchFlat;
|
||||
export type SearchOptions = SearchTuple[] | GroupedOptions;
|
||||
|
||||
// These can safely be ignored for now.
|
||||
export type Group<T> = [string, T[]];
|
||||
|
||||
export type ElementRendererBase<T> = (element: T) => string;
|
||||
export type ElementRenderer<T, S = keyof T> = ElementRendererBase<T> | S;
|
||||
|
||||
export type DescriptionRendererBase<T> = (element: T) => TemplateResult | string;
|
||||
export type DescriptionRenderer<T, S = keyof T> = ElementRendererBase<T> | S;
|
||||
|
||||
export type ValueExtractorBase<T> = (element: T | undefined) => keyof T | undefined;
|
||||
export type ValueExtractor<T, S = keyof T> = ValueExtractorBase<T> | S;
|
||||
|
||||
export type ValueSelectorBase<T> = (element: T, elements: T[]) => boolean;
|
||||
export type ValueSelector<T, S extends keyof T> = S extends S
|
||||
? ValueSelectorBase<T> | [T, T[S]]
|
||||
: never;
|
||||
|
||||
export type GroupByBase<T> = (elements: T[]) => Group<T>[];
|
||||
export type GroupBy<T, S = keyof T> = GroupByBase<T> | keyof S;
|
||||
@@ -117,19 +117,11 @@ export class SidebarItem extends AKElement {
|
||||
if (!this.path) {
|
||||
return false;
|
||||
}
|
||||
if (this.path) {
|
||||
const ourPath = this.path.split(";")[0];
|
||||
if (new RegExp(`^${ourPath}$`).exec(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.activeMatchers.some((v) => {
|
||||
const match = v.exec(path);
|
||||
if (match !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const ourPath = this.path.split(";")[0];
|
||||
const pathIsWholePath = new RegExp(`^${ourPath}$`).test(path);
|
||||
const pathIsAnActivePath = this.activeMatchers.some((v) => v.test(path));
|
||||
return pathIsWholePath || pathIsAnActivePath;
|
||||
}
|
||||
|
||||
expandParentRecursive(activePath: string, item: SidebarItem): void {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -19,10 +20,12 @@ export enum TypeCreateWizardPageLayouts {
|
||||
grid = "grid",
|
||||
}
|
||||
|
||||
type TypeCreateWithTestId = TypeCreate & { testId?: string };
|
||||
|
||||
@customElement("ak-wizard-page-type-create")
|
||||
export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
@property({ attribute: false })
|
||||
types: TypeCreate[] = [];
|
||||
types: TypeCreateWithTestId[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
selectedType?: TypeCreate;
|
||||
@@ -51,7 +54,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
|
||||
sidebarLabel = () => msg("Select type");
|
||||
|
||||
activeCallback: () => Promise<void> = async () => {
|
||||
activeCallback = async () => {
|
||||
this.host.isValid = false;
|
||||
if (this.selectedType) {
|
||||
this.selectDispatch(this.selectedType);
|
||||
@@ -78,6 +81,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
: "pf-m-selectable-raised"} ${this.selectedType == type
|
||||
? "pf-m-selected-raised"
|
||||
: ""}"
|
||||
data-testid=${ifDefined(type.testId)}
|
||||
tabindex=${idx}
|
||||
@click=${() => {
|
||||
if (requiresEnterprise) {
|
||||
|
||||
181
web/src/flow/components/ak-flow-password-input.ts
Normal file
181
web/src/flow/components/ak-flow-password-input.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing, render } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-flow-input-password")
|
||||
export class InputPassword extends AKElement {
|
||||
static get styles() {
|
||||
return [PFBase, PFInputGroup, PFFormControl, PFButton];
|
||||
}
|
||||
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
inputId = "ak-stage-password-input";
|
||||
|
||||
@property({ type: String })
|
||||
name = "password";
|
||||
|
||||
@property({ type: String })
|
||||
label = msg("Password");
|
||||
|
||||
@property({ type: String })
|
||||
placeholder = msg("Please enter your password");
|
||||
|
||||
@property({ type: String, attribute: "prefill" })
|
||||
passwordPrefill = "";
|
||||
|
||||
@property({ type: Object })
|
||||
errors: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Forwarded to the input tag's aria-invalid attribute, if set
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
invalid?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-show-password" })
|
||||
allowShowPassword = false;
|
||||
|
||||
/**
|
||||
* Automatically grab focus after rendering.
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "grab-focus" })
|
||||
grabFocus = false;
|
||||
|
||||
timer?: number;
|
||||
|
||||
input?: HTMLInputElement;
|
||||
|
||||
cleanup(): void {
|
||||
if (this.timer) {
|
||||
console.debug("authentik/stages/password: cleared focus timer");
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Must support both older browsers and shadyDom; we'll keep using this in-line, but it'll still
|
||||
// be in the scope of the parent element, not an independent shadowDOM.
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
// State is saved in the DOM, and read from the DOM. Directly affects the DOM,
|
||||
// so no `.requestUpdate()` required. Effect is immediately visible.
|
||||
togglePasswordVisibility(ev: PointerEvent) {
|
||||
const passwordField = this.renderRoot.querySelector(`#${this.inputId}`) as HTMLInputElement;
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
if (!passwordField) {
|
||||
throw new Error("ak-flow-password-input: unable to identify input field");
|
||||
}
|
||||
|
||||
passwordField.type = passwordField.type === "password" ? "text" : "password";
|
||||
this.renderPasswordVisibilityFeatures(passwordField);
|
||||
}
|
||||
|
||||
// In the unlikely event that we want to make "show password" the _default_ behavior, this
|
||||
// effect handler is broken out into its own method. The current behavior in the main
|
||||
// `.render()` method assumes the field is of type "password." To have this effect, er, take
|
||||
// effect, call it in an `.updated()` method.
|
||||
renderPasswordVisibilityFeatures(passwordField: HTMLInputElement) {
|
||||
const toggleId = `#${this.inputId}-visibility-toggle`;
|
||||
const visibilityToggle = this.renderRoot.querySelector(toggleId) as HTMLButtonElement;
|
||||
if (!visibilityToggle) {
|
||||
return;
|
||||
}
|
||||
const show = passwordField.type === "password";
|
||||
visibilityToggle?.setAttribute(
|
||||
"aria-label",
|
||||
show ? msg("Show password") : msg("Hide password"),
|
||||
);
|
||||
visibilityToggle?.querySelector("i")?.remove();
|
||||
render(
|
||||
show
|
||||
? html`<i class="fas fa-eye" aria-hidden="true"></i>`
|
||||
: html`<i class="fas fa-eye-slash" aria-hidden="true"></i>`,
|
||||
visibilityToggle,
|
||||
);
|
||||
}
|
||||
|
||||
renderInput(): HTMLInputElement {
|
||||
this.input = document.createElement("input");
|
||||
this.input.id = `${this.inputId}`;
|
||||
this.input.type = "password";
|
||||
this.input.name = this.name;
|
||||
this.input.placeholder = this.placeholder;
|
||||
this.input.autofocus = true;
|
||||
this.input.autocomplete = "current-password";
|
||||
this.input.classList.add("pf-c-form-control");
|
||||
this.input.required = true;
|
||||
this.input.value = this.passwordPrefill ?? "";
|
||||
if (this.invalid) {
|
||||
this.input.setAttribute("aria-invalid", this.invalid);
|
||||
}
|
||||
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
|
||||
// isn't enough, due to timing within shadow doms and such.
|
||||
|
||||
if (this.grabFocus) {
|
||||
this.timer = window.setInterval(() => {
|
||||
if (!this.input) {
|
||||
return;
|
||||
}
|
||||
// Because activeElement behaves differently with shadow dom
|
||||
// we need to recursively check
|
||||
const rootEl = document.activeElement;
|
||||
const isActive = (el: Element | null): boolean => {
|
||||
if (!rootEl) return false;
|
||||
if (!("shadowRoot" in rootEl)) return false;
|
||||
if (rootEl.shadowRoot === null) return false;
|
||||
if (rootEl.shadowRoot.activeElement === el) return true;
|
||||
return isActive(rootEl.shadowRoot.activeElement);
|
||||
};
|
||||
if (isActive(this.input)) {
|
||||
this.cleanup();
|
||||
}
|
||||
this.input.focus();
|
||||
}, 10);
|
||||
console.debug("authentik/stages/password: started focus timer");
|
||||
}
|
||||
return this.input;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <ak-form-element
|
||||
label="${this.label}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${this.errors}
|
||||
>
|
||||
<div class="pf-c-input-group">
|
||||
${this.renderInput()}
|
||||
${this.allowShowPassword
|
||||
? html` <button
|
||||
class="pf-c-button pf-m-control ak-stage-password-toggle-visibility"
|
||||
type="button"
|
||||
aria-label=${msg("Show password")}
|
||||
@click=${(ev: PointerEvent) => this.togglePasswordVisibility(ev)}
|
||||
>
|
||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ak-form-element>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-input-password": InputPassword;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<p>${msg("You're about to be redirect to the following URL.")}</p>
|
||||
<p>${msg("You will now be redirected to the following URL.")}</p>
|
||||
<code>${this.getURL()}</code>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { renderSourceIcon } from "@goauthentik/admin/sources/utils";
|
||||
import "@goauthentik/elements/Divider";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import "@goauthentik/flow/components/ak-flow-password-input.js";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -12,6 +13,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -45,22 +47,32 @@ export class IdentificationStage extends BaseStage<
|
||||
form?: HTMLFormElement;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
|
||||
return [
|
||||
PFBase,
|
||||
PFAlert,
|
||||
PFInputGroup,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
/* login page's icons */
|
||||
.pf-c-login__main-footer-links-item button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.pf-c-login__main-footer-links-item img {
|
||||
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
|
||||
width: 100px;
|
||||
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
|
||||
height: 100%;
|
||||
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
||||
}
|
||||
`);
|
||||
css`
|
||||
.pf-c-login__main-footer-links-item button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.pf-c-login__main-footer-links-item img {
|
||||
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
|
||||
width: 100px;
|
||||
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
|
||||
height: 100%;
|
||||
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
@@ -250,22 +262,16 @@ export class IdentificationStage extends BaseStage<
|
||||
</ak-form-element>
|
||||
${this.challenge.passwordFields
|
||||
? html`
|
||||
<ak-form-element
|
||||
label="${msg("Password")}"
|
||||
?required="${true}"
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
inputId="ak-stage-identification-password"
|
||||
required
|
||||
grab-focus
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge.responseErrors || {})["password"]}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="${msg("Password")}"
|
||||
autocomplete="current-password"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
value=${PasswordManagerPrefill.password || ""}
|
||||
/>
|
||||
</ak-form-element>
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
?allow-show-password=${this.challenge.allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill["password"] ?? ""}
|
||||
></ak-flow-input-password>
|
||||
`
|
||||
: nothing}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import "@goauthentik/flow/components/ak-flow-password-input.js";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
||||
|
||||
@@ -12,6 +13,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -21,62 +23,14 @@ import { PasswordChallenge, PasswordChallengeResponseRequest } from "@goauthenti
|
||||
@customElement("ak-stage-password")
|
||||
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle];
|
||||
return [PFBase, PFLogin, PFInputGroup, PFForm, PFFormControl, PFButton, PFTitle];
|
||||
}
|
||||
|
||||
input?: HTMLInputElement;
|
||||
|
||||
timer?: number;
|
||||
|
||||
hasError(field: string): boolean {
|
||||
const errors = (this.challenge?.responseErrors || {})[field];
|
||||
return (errors || []).length > 0;
|
||||
}
|
||||
|
||||
renderInput(): HTMLInputElement {
|
||||
this.input = document.createElement("input");
|
||||
this.input.type = "password";
|
||||
this.input.name = "password";
|
||||
this.input.placeholder = msg("Please enter your password");
|
||||
this.input.autofocus = true;
|
||||
this.input.autocomplete = "current-password";
|
||||
this.input.classList.add("pf-c-form-control");
|
||||
this.input.required = true;
|
||||
this.input.value = PasswordManagerPrefill.password || "";
|
||||
this.input.setAttribute("aria-invalid", this.hasError("password").toString());
|
||||
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
|
||||
// isn't enough, due to timing within shadow doms and such.
|
||||
this.timer = window.setInterval(() => {
|
||||
if (!this.input) {
|
||||
return;
|
||||
}
|
||||
// Because activeElement behaves differently with shadow dom
|
||||
// we need to recursively check
|
||||
const rootEl = document.activeElement;
|
||||
const isActive = (el: Element | null): boolean => {
|
||||
if (!rootEl) return false;
|
||||
if (!("shadowRoot" in rootEl)) return false;
|
||||
if (rootEl.shadowRoot === null) return false;
|
||||
if (rootEl.shadowRoot.activeElement === el) return true;
|
||||
return isActive(rootEl.shadowRoot.activeElement);
|
||||
};
|
||||
if (isActive(this.input)) {
|
||||
this.cleanup();
|
||||
}
|
||||
this.input.focus();
|
||||
}, 10);
|
||||
console.debug("authentik/stages/password: started focus timer");
|
||||
return this.input;
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
if (this.timer) {
|
||||
console.debug("authentik/stages/password: cleared focus timer");
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
@@ -109,14 +63,16 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
type="hidden"
|
||||
value="${this.challenge.pendingUser}"
|
||||
/>
|
||||
<ak-form-element
|
||||
label="${msg("Password")}"
|
||||
?required="${true}"
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
required
|
||||
grab-focus
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
>
|
||||
${this.renderInput()}
|
||||
</ak-form-element>
|
||||
?allow-show-password=${this.challenge.allowShowPassword}
|
||||
invalid=${this.hasError("password").toString()}
|
||||
prefill=${PasswordManagerPrefill["password"] ?? ""}
|
||||
></ak-flow-input-password>
|
||||
|
||||
${this.challenge.recoveryUrl
|
||||
? html`<a href="${this.challenge.recoveryUrl}">
|
||||
|
||||
@@ -231,14 +231,11 @@ ${prompt.initialValue}</textarea
|
||||
|
||||
shouldRenderInWrapper(prompt: StagePrompt): boolean {
|
||||
// Special types that aren't rendered in a wrapper
|
||||
if (
|
||||
return !(
|
||||
prompt.type === PromptTypeEnum.Static ||
|
||||
prompt.type === PromptTypeEnum.Hidden ||
|
||||
prompt.type === PromptTypeEnum.Separator
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
);
|
||||
}
|
||||
|
||||
renderField(prompt: StagePrompt): TemplateResult {
|
||||
|
||||
@@ -166,6 +166,6 @@ export class LibraryPageApplicationSearch extends AKElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-library-list-search": LibraryPageApplicationList;
|
||||
"ak-library-list-search": LibraryPageApplicationSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,7 @@ export class UserSettingsFlowExecutor
|
||||
})
|
||||
.then((data) => {
|
||||
this.challenge = data;
|
||||
if (this.challenge.responseErrors) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !this.challenge.responseErrors;
|
||||
})
|
||||
.catch((e: Error | ResponseError) => {
|
||||
this.errorMessage(e);
|
||||
|
||||
@@ -10,20 +10,16 @@ import { PromptTypeEnum, StagePrompt } from "@goauthentik/api";
|
||||
@customElement("ak-user-stage-prompt")
|
||||
export class UserSettingsPromptStage extends PromptStage {
|
||||
renderPromptInner(prompt: StagePrompt): TemplateResult {
|
||||
switch (prompt.type) {
|
||||
// Checkbox requires slightly different rendering here due to the use of horizontal form elements
|
||||
case PromptTypeEnum.Checkbox:
|
||||
return html`<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
name="${prompt.fieldKey}"
|
||||
?checked=${prompt.initialValue !== ""}
|
||||
?required=${prompt.required}
|
||||
style="vertical-align: bottom"
|
||||
/>`;
|
||||
default:
|
||||
return super.renderPromptInner(prompt);
|
||||
}
|
||||
return prompt.type === PromptTypeEnum.Checkbox
|
||||
? html`<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
name="${prompt.fieldKey}"
|
||||
?checked=${prompt.initialValue !== ""}
|
||||
?required=${prompt.required}
|
||||
style="vertical-align: bottom"
|
||||
/>`
|
||||
: super.renderPromptInner(prompt);
|
||||
}
|
||||
|
||||
renderField(prompt: StagePrompt): TemplateResult {
|
||||
|
||||
0
website/scripts/README.md
Normal file
0
website/scripts/README.md
Normal file
Reference in New Issue
Block a user