Compare commits
6 Commits
web/testin
...
docs-scrip
| Author | SHA1 | Date | |
|---|---|---|---|
| 78bc5687ef | |||
| 52b1e8de13 | |||
| 73a9957925 | |||
| 90824b966f | |||
| 4c9f977361 | |||
| 9143197ef5 |
@ -45,7 +45,6 @@ class TestFlowInspector(APITestCase):
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{
|
||||
"allow_show_password": False,
|
||||
"component": "ak-stage-identification",
|
||||
"flow_info": {
|
||||
"background": flow.background_url,
|
||||
|
||||
@ -38,11 +38,10 @@ class IdentificationStage(Stage):
|
||||
help_text=_(
|
||||
(
|
||||
"When set, shows a password field, instead of showing the "
|
||||
"password field as separate step."
|
||||
"password field as seaprate step."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
case_insensitive_matching = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("When enabled, user fields are matched regardless of their casing."),
|
||||
|
||||
@ -64,7 +64,6 @@ 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)
|
||||
|
||||
@ -198,8 +197,6 @@ 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,7 +16,6 @@ class PasswordStageSerializer(StageSerializer):
|
||||
"backends",
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
"allow_show_password",
|
||||
]
|
||||
|
||||
|
||||
@ -29,7 +28,6 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"name",
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
"allow_show_password",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
# 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,12 +43,6 @@ 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 BooleanField, CharField
|
||||
from rest_framework.fields import CharField
|
||||
from sentry_sdk.hub import Hub
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -76,8 +76,6 @@ class PasswordChallenge(WithUserInfoChallenge):
|
||||
|
||||
component = CharField(default="ak-stage-password")
|
||||
|
||||
allow_show_password = BooleanField(default=False)
|
||||
|
||||
|
||||
class PasswordChallengeResponse(ChallengeResponse):
|
||||
"""Password challenge response"""
|
||||
@ -136,11 +134,7 @@ class PasswordStageView(ChallengeStageView):
|
||||
response_class = PasswordChallengeResponse
|
||||
|
||||
def get_challenge(self) -> Challenge:
|
||||
challenge = PasswordChallenge(
|
||||
data={
|
||||
"allow_show_password": self.executor.current_stage.allow_show_password,
|
||||
}
|
||||
)
|
||||
challenge = PasswordChallenge(data={})
|
||||
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 separate step."
|
||||
"description": "When set, shows a password field, instead of showing the password field as seaprate step."
|
||||
},
|
||||
"case_insensitive_matching": {
|
||||
"type": "boolean",
|
||||
@ -7207,11 +7207,6 @@
|
||||
"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.3
|
||||
goauthentik.io/api/v3 v3.2024061.2
|
||||
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.3 h1:gbP8mhHE2/iCDSZEnAUvRkh9DQhggdTfhsEYKg3sp/U=
|
||||
goauthentik.io/api/v3 v3.2024061.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024061.2 h1:9NHK2wriMENQHUmbYN3uxsdZZIV0QoEEEaGM0JS8XRY=
|
||||
goauthentik.io/api/v3 v3.2024061.2/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.10.0"
|
||||
version = "2.9.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
28
schema.yml
28
schema.yml
@ -29993,10 +29993,6 @@ paths:
|
||||
operationId: stages_password_list
|
||||
description: PasswordStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: allow_show_password
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: configure_flow
|
||||
schema:
|
||||
@ -37071,9 +37067,6 @@ components:
|
||||
nullable: true
|
||||
password_fields:
|
||||
type: boolean
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
default: false
|
||||
application_pre:
|
||||
type: string
|
||||
flow_designation:
|
||||
@ -37156,7 +37149,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as separate step.
|
||||
field as seaprate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@ -37224,7 +37217,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as separate step.
|
||||
field as seaprate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@ -40960,9 +40953,6 @@ components:
|
||||
type: string
|
||||
recovery_url:
|
||||
type: string
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
default: false
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
@ -41245,10 +41235,6 @@ 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
|
||||
@ -41285,10 +41271,6 @@ 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
|
||||
@ -42110,7 +42092,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, shows a password field, instead of showing the password
|
||||
field as separate step.
|
||||
field as seaprate step.
|
||||
case_insensitive_matching:
|
||||
type: boolean
|
||||
description: When enabled, user fields are matched regardless of their casing.
|
||||
@ -42822,10 +42804,6 @@ 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.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@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.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
|
||||
"integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
|
||||
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.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",
|
||||
"@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",
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
|
||||
"integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@ -1004,13 +1004,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
|
||||
"integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
|
||||
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1"
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@ -1021,13 +1021,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
|
||||
"integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.16.1",
|
||||
"@typescript-eslint/utils": "7.16.1",
|
||||
"@typescript-eslint/typescript-estree": "7.16.0",
|
||||
"@typescript-eslint/utils": "7.16.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@ -1048,9 +1048,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
|
||||
"integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
|
||||
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@ -1061,13 +1061,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
|
||||
"integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
|
||||
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/visitor-keys": "7.16.1",
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/visitor-keys": "7.16.0",
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
|
||||
"integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.16.1",
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/typescript-estree": "7.16.1"
|
||||
"@typescript-eslint/scope-manager": "7.16.0",
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"@typescript-eslint/typescript-estree": "7.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@ -1135,12 +1135,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
|
||||
"integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
|
||||
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.16.1",
|
||||
"@typescript-eslint/types": "7.16.0",
|
||||
"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.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@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.$(`>>>div[data-testid=wizard-provider-${value}]`);
|
||||
return this.providerList.$(`>>>input[value="${value}"]`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class ForwardProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'[name="authorizationFlow"]',
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@ import Page from "../page.js";
|
||||
export class LdapForm extends Page {
|
||||
async setBindFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
"[name=authorizationFlow]",
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class OauthForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'[name="authorizationFlow"]',
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@ import Page from "../page.js";
|
||||
export class RadiusForm extends Page {
|
||||
async setAuthenticationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'[name="authorizationFlow"]',
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class SamlForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'[name="authorizationFlow"]',
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,9 @@ import { $ } from "@wdio/globals";
|
||||
export class TransparentProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'[name="authorizationFlow"]',
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`div*=${selector}`,
|
||||
`button*=${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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
|
||||
@ -49,14 +48,6 @@ 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,16 +32,10 @@ export default class Page {
|
||||
*/
|
||||
|
||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||
const controlSelector = `>>>ak-search-select-view${searchSelector}`;
|
||||
const control = await $(controlSelector);
|
||||
control.scrollIntoView();
|
||||
const inputBind = await control.$(">>>input[type=text]");
|
||||
const inputBind = await $(searchSelector);
|
||||
await inputBind.click();
|
||||
|
||||
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
|
||||
const interior = searchBlock.$(">>>ul");
|
||||
interior.scrollIntoView();
|
||||
const target = interior.$(buttonSelector);
|
||||
const target = searchBlock.$(buttonSelector);
|
||||
return await target.click();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
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,9 +212,7 @@ 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) {
|
||||
browser.setWindowSize(1920, 1080);
|
||||
},
|
||||
before: function (_capabilities, _specs) {},
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {string} commandName hook command name
|
||||
|
||||
30
web/.eslintrc.precommit.json
Normal file
30
web/.eslintrc.precommit.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"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,10 +16,9 @@
|
||||
"@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-1721092506",
|
||||
"@goauthentik/api": "^2024.6.1-1720888668",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@ -49,7 +48,7 @@
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.9",
|
||||
"@babel/core": "^7.24.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
@ -65,13 +64,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@spotlightjs/spotlight": "^2.0.0",
|
||||
"@storybook/addon-essentials": "^8.2.4",
|
||||
"@storybook/addon-links": "^8.2.4",
|
||||
"@storybook/addon-essentials": "^8.2.2",
|
||||
"@storybook/addon-links": "^8.2.2",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.2.4",
|
||||
"@storybook/web-components": "^8.2.4",
|
||||
"@storybook/web-components-vite": "^8.2.4",
|
||||
"@storybook/manager-api": "^8.2.2",
|
||||
"@storybook/web-components": "^8.2.2",
|
||||
"@storybook/web-components-vite": "^8.2.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@ -93,14 +92,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.3",
|
||||
"prettier": "^3.3.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -169,21 +168,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz",
|
||||
"integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.24.9",
|
||||
"@babel/generator": "^7.24.8",
|
||||
"@babel/helper-compilation-targets": "^7.24.8",
|
||||
"@babel/helper-module-transforms": "^7.24.9",
|
||||
"@babel/helper-module-transforms": "^7.24.8",
|
||||
"@babel/helpers": "^7.24.8",
|
||||
"@babel/parser": "^7.24.8",
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.8",
|
||||
"@babel/types": "^7.24.9",
|
||||
"@babel/types": "^7.24.8",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@ -199,12 +198,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.9.tgz",
|
||||
"integrity": "sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A==",
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz",
|
||||
"integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.24.9",
|
||||
"@babel/types": "^7.24.8",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^2.5.1"
|
||||
@ -364,9 +363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.24.7",
|
||||
@ -2046,9 +2045,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
|
||||
"integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz",
|
||||
"integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
@ -3674,9 +3673,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.6.1-1721092506",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
|
||||
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
|
||||
"version": "2024.6.1-1720888668",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
|
||||
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
|
||||
},
|
||||
"node_modules/@hcaptcha/types": {
|
||||
"version": "1.0.3",
|
||||
@ -6587,9 +6586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-actions": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.4.tgz",
|
||||
"integrity": "sha512-l1dlzWBBkR/5aullsX8N1ZbYr2bkeHPAaMCRy1jG5BBA8IHbi55JFwmJ8XF2gXkT2GyAZnePzb43RuLXz4KxFQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.2.tgz",
|
||||
"integrity": "sha512-SN4cSRt3f0qXi5te+yhMseSdQuZntA8lGlASbRmN77YQTpIaGsNiH88xFoky0s9qz531hiRfU1R0ZSMylBwSKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@ -6603,13 +6602,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-backgrounds": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.4.tgz",
|
||||
"integrity": "sha512-4oU25rFyr4OgMxHe4RpLJ7lxVwUDfdTi1j/YVyHfYv8koTqjagso8bv0uj0ujP5C3dSsVO0sp3/JOfPDkEUtrA==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@ -6621,13 +6620,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-controls": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.4.tgz",
|
||||
"integrity": "sha512-e56aUYhxyR8zJJstRAUP3WILhWTcvgRf5bysTtiyjFAL7U47cuCr043+IYEsxLkXhuZTKX2pcYSrjBtT5bYkVA==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.2.tgz",
|
||||
"integrity": "sha512-y241aOANGzT5XBADUIvALwG/xF5eC6UItzmWJaFvOzSBCq74GIA0+Hu9atyFdvFQbXOrdvPWC4jR+9iuBFRxAA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.2",
|
||||
@ -6639,21 +6638,21 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.4.tgz",
|
||||
"integrity": "sha512-oyrDw4nGfntu5Hkhr2Qt1wUOyLaVVERQekYyejyir92QhM10UeA7ZarPXNLfCTj7rbTrWmM1Waka9Tsf8TGMrw==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.2.tgz",
|
||||
"integrity": "sha512-qk/yjAR9RpsSrKLLbeCgb6u58c8TmYqyJSnXgbAozZZNKHBWlIpvZ/hTNYud8qo0coPlxnLdjnZf32TykWGlAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/blocks": "8.2.4",
|
||||
"@storybook/csf-plugin": "8.2.4",
|
||||
"@storybook/blocks": "8.2.2",
|
||||
"@storybook/csf-plugin": "8.2.2",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "8.2.4",
|
||||
"@storybook/react-dom-shim": "8.2.2",
|
||||
"@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",
|
||||
@ -6667,7 +6666,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/fs-extra": {
|
||||
@ -6685,20 +6684,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-essentials": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.4.tgz",
|
||||
"integrity": "sha512-4upNauDJAJxauxnoUpUvzDnLo18C2yTVxgg+Id9wrKpt9C+CYH2oXyXzxoYGucYWZEe7zgCO6rWrGrKEisiLPQ==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@ -6706,13 +6705,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-highlight": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.4.tgz",
|
||||
"integrity": "sha512-Ll/2y0m/q9ko9jFt40qsiee4fds6vpcwwxi3mPAVwRV/J7PpMzPkoLxM54bKpeHiWdTeGCXRguXNvyeQMQf3pg==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
@ -6722,13 +6721,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.4.tgz",
|
||||
"integrity": "sha512-1FgD6YXdXXSEDrp2aO4LxYt/X7LnBYx7cLlFla+xbn1CZLGqWLLeOT+BFd29wxpzs3u1Tap9r1iz1vRYL5ziyg==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.2.tgz",
|
||||
"integrity": "sha512-eGh7O7SgTJMtnuXC0HlRPOegu1njcJS2cnVqjbzjvjxsPSBhbHpdYMi9Q9E7al/FKuqMUOjIR9YLIlmK1AJaqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@ -6741,7 +6740,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@ -6750,9 +6749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-measure": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.4.tgz",
|
||||
"integrity": "sha512-bSyE3mGDaaIKoe6Kt/f20YXKsn8WSoJUHrfKA68gbb+H3tegVQaqeS2KY5YzLqvjHe1qSmrO132NJt8RixLOPQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.2.tgz",
|
||||
"integrity": "sha512-3rCo/aMltt5FrBVdr2dYlD8HlE2q9TLKGJZnwh9on4QyL6ArHbdYw0LmyHe/LrFahJ49w1XQZBMSJcAdRkkS7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@ -6763,13 +6762,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-outline": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.4.tgz",
|
||||
"integrity": "sha512-1C6NrvSDREgCZ7o/1n7Ca81uDDzrSrzWiOkh4OeA7PPQ/445cAOX2OMvxzNkKDIT9GLCLNi9M5XIVyGxJVS4dQ==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@ -6780,26 +6779,26 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-toolbars": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.4.tgz",
|
||||
"integrity": "sha512-iPnSr+hdz40Uoqg2cimyWf01/Y8GdgdMKB+b47TGIxtn9SEFBXck00ZG8ttwBvEsecu9K9CDt20fIOnr6oK5tQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.2.tgz",
|
||||
"integrity": "sha512-JGOueOc3EPljlCl9dVSQee0aMYoqGNvN0UH+R6wYJ3bDZ+tUG/iYpsZVPUOvS8vzp3Imk5Is1kzQbQYJtzdGLg==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-viewport": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.4.tgz",
|
||||
"integrity": "sha512-58DcoX0xGpWlJfc0iLDjggkVPYzT4JdCZA2ioK9SQXQMsUzGFwR5PAAJv1tivYp7467tNkXvcM3QTb3Q3g8p4g==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"memoizerific": "^1.11.3"
|
||||
@ -6809,7 +6808,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addons": {
|
||||
@ -7102,9 +7101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/blocks": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.4.tgz",
|
||||
"integrity": "sha512-Hl2Dpg41YiJLSVXxjEJPjgPShrDJM3RY6HEEOjqTcAADsheX1IHAWXMJSJGMmne3Sew6VdJXPuHBIOFV4suZxg==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.2.tgz",
|
||||
"integrity": "sha512-av0Tryg4toDl2L/d1ABErtsAk9wvM1su6+M4wq5/Go50sk5IjGTldhbZFa9zNOohxLkZwaj0Q5xAgJ1Y+m5KrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@ -7129,7 +7128,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.4"
|
||||
"storybook": "^8.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@ -7141,12 +7140,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.4.tgz",
|
||||
"integrity": "sha512-hDx0ZLcnFrIJaVoFMu41d9w1uWmwy/DDUuIbSd0T7xHwWyVqgI8lmaQlBIp81/QmSKaUB964UduHcdIjkoWoYA==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.2.2.tgz",
|
||||
"integrity": "sha512-tyt+CjzLEuRHU2NERZSy7JfnTpTJo10HrRysJcRtzclu3TOzx7bWszUJRHho9ttyypBX6w5+8TPcqXh/vu0tig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "8.2.4",
|
||||
"@storybook/csf-plugin": "8.2.2",
|
||||
"@types/find-cache-dir": "^3.2.1",
|
||||
"browser-assert": "^1.2.1",
|
||||
"es-module-lexer": "^1.5.0",
|
||||
@ -7162,7 +7161,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@preact/preset-vite": "*",
|
||||
"storybook": "^8.2.4",
|
||||
"storybook": "^8.2.2",
|
||||
"typescript": ">= 4.3.x",
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite-plugin-glimmerx": "*"
|
||||
@ -7235,15 +7234,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/codemod": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.4.tgz",
|
||||
"integrity": "sha512-QcZdqjX4NvkVcWR3yI9it3PfqmBOCR+3iY6j4PmG7p5IE0j9kXMKBbeFrBRprSijHKlwcjbc3bRx2SnKF6AFEg==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.2.tgz",
|
||||
"integrity": "sha512-wRUVKLHVUhbLJYKW3QOufUxJGwaUT4jTCD8+HOGpHPdJO3NrwXu186xt4tuPZO2Y/NnacPeCQPsaK5ok4O8o7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@storybook/core": "8.2.4",
|
||||
"@storybook/core": "8.2.2",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"cross-spawn": "^7.0.3",
|
||||
@ -7333,9 +7332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.4.tgz",
|
||||
"integrity": "sha512-jePmsGZT2hhUNQs8ED6+hFVt2m4hrMseO8kkN7Mcsve1MIujzHUS7Gjo4uguBwHJJOtiXB2fw4OSiQCmsXscZA==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.2.tgz",
|
||||
"integrity": "sha512-L4ojYI+Os/i5bCReDIlFgEDQSS94mbJlNU9WRzEGZpqNC5/hbFEC9Tip7P1MiRx9NrewkzU7b+UCP7mi3e4drQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.11",
|
||||
@ -7457,9 +7456,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"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==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.2.tgz",
|
||||
"integrity": "sha512-3K2RUpDDvq3DT46qAIj2VBC+fzTTebRUcZUsRfS6G1AzaX9p25iClEHiwcJacFkgQKhkci8A/Ly3Z4JJ3b4Pgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"unplugin": "^1.3.1"
|
||||
@ -7469,7 +7468,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/global": {
|
||||
@ -7490,35 +7489,107 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/manager-api": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.2.4.tgz",
|
||||
"integrity": "sha512-ayiOtcGupSeLCi2doEsRpALNPo4MBWYruc+e3jjkeVJQIg9A1ipSogNQh8unuOmq9rezO4/vcNBd6MxLs3xLWg==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preview-api": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.2.4.tgz",
|
||||
"integrity": "sha512-IxOiUYYzNnk1OOz3zQBhsa3P1fsgqeMBZcH7TjiQWs9osuWG20oqsFR6+Z3dxoW8IuQHvpnREGKvAbRsDsThcA==",
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.1.11.tgz",
|
||||
"integrity": "sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==",
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.4"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@ -7527,7 +7598,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.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/theming": {
|
||||
@ -7578,16 +7649,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components": {
|
||||
"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==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.2.2.tgz",
|
||||
"integrity": "sha512-6lFesQw9TmdbzgFRytlNqQDPgEqlCQzOvW91ZDyDBdytN64XXONYfNBJqef0tM3hLcXBv1vNIzlnOsMRDhIhZQ==",
|
||||
"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"
|
||||
},
|
||||
@ -7600,17 +7667,17 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^2.0.0 || ^3.0.0",
|
||||
"storybook": "^8.2.4"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components-vite": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.2.4.tgz",
|
||||
"integrity": "sha512-zJwhlYgoMwPHiM7ySLOgTDuNBDH3qPmi+qrvtdpEGVdrSIvijx5jsQQz4XTP2b6BXyOg1g9VaMfQ5S8LaSZ74A==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/builder-vite": "8.2.4",
|
||||
"@storybook/web-components": "8.2.4",
|
||||
"@storybook/builder-vite": "8.2.2",
|
||||
"@storybook/web-components": "8.2.2",
|
||||
"magic-string": "^0.30.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -7621,33 +7688,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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"
|
||||
"storybook": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@ -14422,9 +14463,8 @@
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@ -20512,10 +20552,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"version": "3.3.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@ -22373,15 +22412,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.4.tgz",
|
||||
"integrity": "sha512-ASavW8vIHiWpFY+4M6ngeqK5oL4OkxqdpmQYxvRqH0gA1G1hfq/vmDw4YC4GnqKwyWPQh2kaV5JFurKZVaeaDQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.2.tgz",
|
||||
"integrity": "sha512-xDT9gyzAEFQNeK7P+Mj/8bNzN+fbm6/4D6ihdSzmczayjydpNjMs74HDHMY6S4Bfu6tRVyEK2ALPGnr6ZVofBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@storybook/codemod": "8.2.4",
|
||||
"@storybook/core": "8.2.4",
|
||||
"@storybook/codemod": "8.2.2",
|
||||
"@storybook/core": "8.2.2",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@yarnpkg/fslib": "2.10.3",
|
||||
"@yarnpkg/libzip": "2.3.0",
|
||||
|
||||
@ -43,10 +43,9 @@
|
||||
"@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-1721092506",
|
||||
"@goauthentik/api": "^2024.6.1-1720888668",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@ -76,7 +75,7 @@
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.9",
|
||||
"@babel/core": "^7.24.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
@ -92,13 +91,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@spotlightjs/spotlight": "^2.0.0",
|
||||
"@storybook/addon-essentials": "^8.2.4",
|
||||
"@storybook/addon-links": "^8.2.4",
|
||||
"@storybook/addon-essentials": "^8.2.2",
|
||||
"@storybook/addon-links": "^8.2.2",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.2.4",
|
||||
"@storybook/web-components": "^8.2.4",
|
||||
"@storybook/web-components-vite": "^8.2.4",
|
||||
"@storybook/manager-api": "^8.2.2",
|
||||
"@storybook/web-components": "^8.2.2",
|
||||
"@storybook/web-components-vite": "^8.2.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@ -120,14 +119,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.3",
|
||||
"prettier": "^3.3.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
55
web/scripts/eslint-nightmare.mjs
Normal file
55
web/scripts/eslint-nightmare.mjs
Normal file
@ -0,0 +1,55 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import { ESLint } from "eslint";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
|
||||
// Code assumes this script is in the './web/scripts' folder.
|
||||
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf8",
|
||||
}).replace("\n", "");
|
||||
process.chdir(path.join(projectRoot, "./web"));
|
||||
|
||||
const eslintConfig = {
|
||||
overrideConfig: {
|
||||
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": ["error", 9],
|
||||
"sonarjs/no-duplicate-string": "off",
|
||||
"sonarjs/no-nested-template-literals": "off",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updated = ["./src/", "./build.mjs", "./scripts/*.mjs"];
|
||||
|
||||
const eslint = new ESLint(eslintConfig);
|
||||
const results = await eslint.lintFiles(updated);
|
||||
const formatter = await eslint.loadFormatter("stylish");
|
||||
const resultText = formatter.format(results);
|
||||
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(resultText);
|
||||
process.exit(errors > 1 ? 1 : 0);
|
||||
@ -43,53 +43,34 @@ const eslintConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
function findChangedFiles() {
|
||||
const porcelainV1 = /^(..)\s+(.*$)/;
|
||||
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
|
||||
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);
|
||||
|
||||
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 updated = statuses.reduce(
|
||||
(acc, [status, filename]) =>
|
||||
modified(status) && checkable(filename) && notIgnored(filename)
|
||||
? [...acc, path.join(projectRoot, filename)]
|
||||
: acc,
|
||||
[],
|
||||
);
|
||||
|
||||
const eslint = new ESLint(eslintConfig);
|
||||
const results = await eslint.lintFiles(updated);
|
||||
|
||||
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-1721092506",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1721092506.tgz",
|
||||
"integrity": "sha512-XrRLZz3bsLBuww8FDHqNH4tHr3sqw/B3Bf5LiAexho00ilKSSYqKnuoDVJm16HNeEezc3gVhfZoyVVu/nbGYxA=="
|
||||
"version": "2024.6.1-1720888668",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.1-1720888668.tgz",
|
||||
"integrity": "sha512-3CJl/mS3o5W8F+qfCeFUIjFLRY6xtd1BcjlDcTqMelOIW9VHfh7iVrij1uUW0UE49/siVFw9wwqUXdBxx9pcTA=="
|
||||
},
|
||||
"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,7 +30,6 @@ export type LocalTypeCreate = TypeCreate & {
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
note?: ProviderNote;
|
||||
testId: string;
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
@ -47,7 +46,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-oauth2Provider",
|
||||
iconUrl: "/static/authentik/sources/openidconnect.svg",
|
||||
},
|
||||
{
|
||||
@ -64,7 +62,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-ldapprovider",
|
||||
iconUrl: "/static/authentik/sources/ldap.png",
|
||||
},
|
||||
{
|
||||
@ -80,7 +77,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.Proxy,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-proxy",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@ -96,7 +92,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.ForwardSingle,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-forwardsingle",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@ -112,7 +107,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
mode: ProxyMode.ForwardDomain,
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-proxyprovider-forwarddomain",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
@ -129,7 +123,6 @@ 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",
|
||||
},
|
||||
{
|
||||
@ -144,7 +137,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-samlprovider",
|
||||
iconUrl: "/static/authentik/sources/saml.png",
|
||||
},
|
||||
{
|
||||
@ -159,7 +151,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as RadiusProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-radiusprovider",
|
||||
iconUrl: "/static/authentik/sources/radius.svg",
|
||||
},
|
||||
{
|
||||
@ -174,7 +165,6 @@ export const providerModelsList: LocalTypeCreate[] = [
|
||||
...(provider as SCIMProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
testId: "wizard-provider-scimprovider",
|
||||
iconUrl: "/static/authentik/sources/scim.png",
|
||||
},
|
||||
];
|
||||
|
||||
@ -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 "@goauthentik/components/ak-switch-input.js";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@ -9,6 +9,7 @@ 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,
|
||||
@ -71,10 +72,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 name="name">
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.name || ""}"
|
||||
value="${ifDefined(this.instance?.name || "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
@ -157,7 +158,7 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${this.instance?.failedAttemptsBeforeCancel ?? 5}"
|
||||
value="${first(this.instance?.failedAttemptsBeforeCancel, 5)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
@ -167,12 +168,6 @@ 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>`;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
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 AkControlElement {
|
||||
export class AkMultiSelect extends AKElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataset.akControl = "true";
|
||||
|
||||
145
web/src/components/stories/ak-search-select.stories.ts
Normal file
145
web/src/components/stories/ak-search-select.stories.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { groupBy } from "@goauthentik/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>`,
|
||||
);
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
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(AkControlElement);
|
||||
const AkElementWithCustomEvents = CustomEmitterElement(AKElement);
|
||||
|
||||
/**
|
||||
* @element ak-checkbox-group
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { debounce } from "@goauthentik/elements/utils/debounce";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
@ -26,8 +26,9 @@ import type { DataProvider, DualSelectPair } from "./types";
|
||||
*/
|
||||
|
||||
@customElement("ak-dual-select-provider")
|
||||
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
|
||||
/** A function that takes a page and returns the DualSelectPair[] collection with which to update
|
||||
export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* A function that takes a page and returns the DualSelectPair[] collection with which to update
|
||||
* the "Available" pane.
|
||||
*
|
||||
* @attr
|
||||
@ -83,6 +84,8 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
|
||||
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,6 +3,7 @@ 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";
|
||||
|
||||
@ -73,8 +74,8 @@ export function serializeForm<T extends KeyUnknown>(
|
||||
return;
|
||||
}
|
||||
|
||||
const inputElement = element.querySelector<AkControlElement>("[name]");
|
||||
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) {
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (element.hidden || !inputElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -83,6 +84,10 @@ 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
|
||||
@ -115,6 +120,17 @@ 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);
|
||||
}
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,192 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,286 +0,0 @@
|
||||
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,31 +1,28 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { APIErrorTypes, parseAPIError } from "@goauthentik/common/errors";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { TemplateResult, html, render } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { styleMap } from "lit/directives/style-map.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(AkControlElement) {
|
||||
static get styles() {
|
||||
return [PFBase];
|
||||
}
|
||||
|
||||
export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
// A function which takes the query state object (accepting that it may be empty) and returns a
|
||||
// new collection of objects.
|
||||
@property({ attribute: false })
|
||||
@ -78,10 +75,14 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
|
||||
@property({ attribute: false })
|
||||
selectedObject?: T;
|
||||
|
||||
// Used to inform the form of the name of the object
|
||||
// Not used in this object. No known purpose.
|
||||
@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()
|
||||
@ -92,14 +93,46 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
|
||||
@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();
|
||||
this.dataset.akControl = "true";
|
||||
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);
|
||||
}
|
||||
|
||||
toForm(): unknown {
|
||||
@ -109,16 +142,16 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
|
||||
return this.value(this.selectedObject) || "";
|
||||
}
|
||||
|
||||
json() {
|
||||
return this.toForm();
|
||||
firstUpdated(): void {
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
updateData() {
|
||||
updateData(): void {
|
||||
if (this.isFetchingData) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingData = true;
|
||||
return this.fetchObjects(this.query)
|
||||
this.fetchObjects(this.query)
|
||||
.then((objects) => {
|
||||
objects.forEach((obj) => {
|
||||
if (this.selected && this.selected(obj, objects || [])) {
|
||||
@ -140,97 +173,233 @@ export class SearchSelect<T> extends CustomEmitterElement(AkControlElement) {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
onSearch(event: SearchSelectInputEvent) {
|
||||
if (event.value === undefined) {
|
||||
this.selectedObject = undefined;
|
||||
return;
|
||||
}
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
this.query = event.value;
|
||||
this.updateData()?.then(() => {
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
this.open = false;
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.error) {
|
||||
return html`<em>${msg("Failed to fetch objects: ")} ${this.error.detail}</em>`;
|
||||
}
|
||||
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) {
|
||||
return html`${msg("Loading...")}`;
|
||||
return;
|
||||
}
|
||||
const [shouldRenderGroups, groupedItems] = this.groupedItems;
|
||||
|
||||
const options = this.getGroupedItems();
|
||||
const value = this.selectedObject ? `${this.value(this.selectedObject) ?? ""}` : undefined;
|
||||
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" }),
|
||||
};
|
||||
|
||||
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> `;
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
||||
get renderedValue() {
|
||||
if (this.error) {
|
||||
return msg(str`Failed to fetch objects: ${this.error.detail}`);
|
||||
}
|
||||
if (!this.objects) {
|
||||
return msg("Loading...");
|
||||
}
|
||||
if (this.selectedObject) {
|
||||
return this.renderElement(this.selectedObject);
|
||||
}
|
||||
if (this.blankable) {
|
||||
return this.emptyOption;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
this.renderMenu();
|
||||
|
||||
const onFocus = (ev: FocusEvent) => {
|
||||
this.open = true;
|
||||
this.renderMenu();
|
||||
if (
|
||||
this.blankable &&
|
||||
this.renderedValue === this.emptyOption &&
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
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>`,
|
||||
),
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
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>`,
|
||||
);
|
||||
};
|
||||
@ -1,103 +0,0 @@
|
||||
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>`,
|
||||
);
|
||||
};
|
||||
@ -1,359 +0,0 @@
|
||||
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) ?? [],
|
||||
})),
|
||||
};
|
||||
})();
|
||||
@ -1,66 +0,0 @@
|
||||
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;
|
||||
@ -5,7 +5,6 @@ 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";
|
||||
@ -20,12 +19,10 @@ 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: TypeCreateWithTestId[] = [];
|
||||
types: TypeCreate[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
selectedType?: TypeCreate;
|
||||
@ -54,7 +51,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
|
||||
sidebarLabel = () => msg("Select type");
|
||||
|
||||
activeCallback = async () => {
|
||||
activeCallback: () => Promise<void> = async () => {
|
||||
this.host.isValid = false;
|
||||
if (this.selectedType) {
|
||||
this.selectDispatch(this.selectedType);
|
||||
@ -81,7 +78,6 @@ 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) {
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
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 will now be redirected to the following URL.")}</p>
|
||||
<p>${msg("You're about to be redirect to the following URL.")}</p>
|
||||
<code>${this.getURL()}</code>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
||||
@ -2,7 +2,6 @@ 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";
|
||||
@ -13,7 +12,6 @@ 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";
|
||||
@ -47,32 +45,22 @@ export class IdentificationStage extends BaseStage<
|
||||
form?: HTMLFormElement;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFAlert,
|
||||
PFInputGroup,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
|
||||
/* login page's icons */
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
.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>) {
|
||||
@ -262,16 +250,22 @@ export class IdentificationStage extends BaseStage<
|
||||
</ak-form-element>
|
||||
${this.challenge.passwordFields
|
||||
? html`
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
inputId="ak-stage-identification-password"
|
||||
required
|
||||
grab-focus
|
||||
<ak-form-element
|
||||
label="${msg("Password")}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
?allow-show-password=${this.challenge.allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill["password"] ?? ""}
|
||||
></ak-flow-input-password>
|
||||
.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>
|
||||
`
|
||||
: nothing}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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";
|
||||
|
||||
@ -13,7 +12,6 @@ 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";
|
||||
@ -23,14 +21,62 @@ import { PasswordChallenge, PasswordChallengeResponseRequest } from "@goauthenti
|
||||
@customElement("ak-stage-password")
|
||||
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFInputGroup, PFForm, PFFormControl, PFButton, PFTitle];
|
||||
return [PFBase, PFLogin, 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")}>
|
||||
@ -63,16 +109,14 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
type="hidden"
|
||||
value="${this.challenge.pendingUser}"
|
||||
/>
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
required
|
||||
grab-focus
|
||||
<ak-form-element
|
||||
label="${msg("Password")}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
?allow-show-password=${this.challenge.allowShowPassword}
|
||||
invalid=${this.hasError("password").toString()}
|
||||
prefill=${PasswordManagerPrefill["password"] ?? ""}
|
||||
></ak-flow-input-password>
|
||||
>
|
||||
${this.renderInput()}
|
||||
</ak-form-element>
|
||||
|
||||
${this.challenge.recoveryUrl
|
||||
? html`<a href="${this.challenge.recoveryUrl}">
|
||||
|
||||
77
website/scripts/migration/readme.md
Normal file
77
website/scripts/migration/readme.md
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Docs migration script
|
||||
---
|
||||
|
||||
This repository contains the source code for the authentik Docs Migration Script.
|
||||
|
||||
## How to Run
|
||||
|
||||
- use branch
|
||||
- run the script
|
||||
- ?
|
||||
- test
|
||||
- ?
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- ?
|
||||
- git
|
||||
|
||||
## What does it do, and how does it work?
|
||||
|
||||
The script does the following:
|
||||
|
||||
- migrates all doc files and images from their old file path structure into their new structure
|
||||
- creates new directories (name read from the `migratefile.txt` TO column.)
|
||||
- files moved into new directories
|
||||
- internal links within each file are rewritten to use new location
|
||||
- tbd
|
||||
|
||||
### Setup
|
||||
|
||||
- tbd
|
||||
|
||||
### Run the Script
|
||||
|
||||
`docsmg migrate`
|
||||
|
||||
### Commands:
|
||||
|
||||
- `docsmg move`
|
||||
|
||||
Examples:
|
||||
|
||||
`docsmg move + <current path file name> <target path and file name>`
|
||||
|
||||
or for moving all files within a folder, use:
|
||||
|
||||
`docsmg move testing/testing2 newtesting/newtesting2/puthere >> migratefile`
|
||||
|
||||
- `docsmg migrate`
|
||||
|
||||
This will read the `migratefile.txt`, creates the dirs (gets the names from the `migratefile.txt` file), and then migrate the files.
|
||||
|
||||
- `docsmg unmigrate`
|
||||
|
||||
This command will undo the most recent move.
|
||||
|
||||
#### Flags
|
||||
|
||||
Flags include
|
||||
|
||||
Use `-r` if we are keeping the exact same structure in any place, we can use this flag to move that specified dir to a new place while keeping the sub-structure exactly the same.
|
||||
|
||||
Using `-m` flag that allows tab-completion of the OLD file path.
|
||||
|
||||
Use `-q` flag with `docsmg migrate` to not show the successful lines but will show any failures.
|
||||
|
||||
### Steps:
|
||||
|
||||
1. tbw
|
||||
- sub-tbw
|
||||
- sub-tbq
|
||||
2. tbw
|
||||
3. tbw
|
||||
4. tbw
|
||||
|
||||
## Verify the migration
|
||||
11
website/scripts/readme.md
Normal file
11
website/scripts/readme.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
title: Docs Tools
|
||||
---
|
||||
|
||||
This repository is for tools that the Docs teams uses for various tasks:
|
||||
|
||||
## Docs migration script:
|
||||
|
||||
This [script](./migration/readme.md) will be used to auto-build the new structure for the docs, as part of the Information Architecture effort.
|
||||
|
||||
Using a script that will restucture all of our docs at once prevents the difficulty of trying to handle pull requests not being able to be merged while working on the docs restructure.
|
||||
Reference in New Issue
Block a user