From 7c4fafdc28673a2a8752d742991f8335ec79bb0c Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Tue, 2 Jan 2024 15:46:28 -0800 Subject: [PATCH] web: provide a "select / select all" tool for the dual list multiselect **This commit** - Fixes the bug whereby pagination would leave the 'some moves available' state visible by clearing the 'to-move' state when the list of options changes. - Fixes the bug whereby a change of 'options' in available would also cause an update to `selectedKeys`, causing the entire selected field to clear. Fixed by making `selectedKeys` a static object updated only when `selected` is generated rather than generating it anew with each re-rerender. (Hey, kids, can you say "functional programming and immutability" five time fast? I knew you could!) - Fixes the bug whereby the change of outpost type would not cause an update of the `options` collection. - Fixes the bug whereby the CSS was not creating enough whitespace separation between the whole component and its siblings. Host components are coded `span:static` unless otherwise styled to be `block`; we want `block` most of the time. - Fixes the bug whereby the list of existing objects wasn't being passed to the handler correctly. - Updates the Form Handler to recognize this new input object. - Fixes the bug whereby changing outpost type doesn't handle the list of selected applications well. - Fixes the bug whereby the identity of the outpost type's associated `fetch()` function loses identity -- necessary to maintain the selected outpost type switch. - Fixes the CSS bug whereby horizontal scrolling would not enable correctly when the application's name overflows the listbox. - Completes this assignment. :-) --- tests/wdio/package-lock.json | 220 ++++++++++++++++-- tests/wdio/package.json | 3 + tests/wdio/wdio.conf.ts | 5 +- web/src/admin/outposts/OutpostForm.ts | 125 +++++----- .../ak-dual-select/ak-dual-select-provider.ts | 115 +++++++++ .../elements/ak-dual-select/ak-dual-select.ts | 100 ++++---- .../ak-dual-select-available-pane.ts | 109 ++++----- .../components/ak-dual-select-controls.ts | 18 +- .../ak-dual-select-selected-pane.ts | 75 +++--- .../components/ak-pagination.ts | 4 +- .../ak-dual-select/components/styles.css.ts | 90 +++++-- web/src/elements/ak-dual-select/index.ts | 5 +- web/src/elements/ak-dual-select/types.ts | 7 + web/src/elements/forms/Form.ts | 7 +- .../elements/forms/HorizontalFormElement.ts | 39 +++- 15 files changed, 657 insertions(+), 265 deletions(-) create mode 100644 web/src/elements/ak-dual-select/ak-dual-select-provider.ts diff --git a/tests/wdio/package-lock.json b/tests/wdio/package-lock.json index c9ce79620..4b69d718e 100644 --- a/tests/wdio/package-lock.json +++ b/tests/wdio/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "@goauthentik/web-tests", + "dependencies": { + "chromedriver": "^120.0.1" + }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@typescript-eslint/eslint-plugin": "^6.16.0", @@ -786,6 +789,11 @@ "node": ">=14.16" } }, + "node_modules/@testim/chrome-version": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", + "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -885,7 +893,7 @@ "version": "20.7.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.0.tgz", "integrity": "sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg==", - "dev": true + "devOptional": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.2", @@ -939,7 +947,6 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", - "dev": true, "optional": true, "dependencies": { "@types/node": "*" @@ -1710,6 +1717,11 @@ "node": ">=0.12.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1722,6 +1734,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.3.tgz", + "integrity": "sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -1867,7 +1889,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "engines": { "node": "*" } @@ -2022,6 +2043,50 @@ "fsevents": "~2.3.2" } }, + "node_modules/chromedriver": { + "version": "120.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-120.0.1.tgz", + "integrity": "sha512-ETTJlkibcAmvoKsaEoq2TFqEsJw18N0O9gOQZX6Uv/XoEiOV8p+IZdidMeIRYELWJIgCZESvlOx5d1QVnB4v0w==", + "hasInstallScript": true, + "dependencies": { + "@testim/chrome-version": "^1.1.4", + "axios": "^1.6.0", + "compare-versions": "^6.1.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.2" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chromedriver/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/chromedriver/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chromium-bidi": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", @@ -2182,6 +2247,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -2191,6 +2267,11 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/compare-versions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", + "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==" + }, "node_modules/compress-commons": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", @@ -2338,7 +2419,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2393,8 +2473,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge-ts": { "version": "5.1.0", @@ -2471,6 +2550,14 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2683,7 +2770,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -3218,7 +3304,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -3238,7 +3323,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "dependencies": { "pump": "^3.0.0" }, @@ -3302,7 +3386,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "dependencies": { "pend": "~1.2.0" } @@ -3457,6 +3540,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3482,6 +3584,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -4279,6 +4394,14 @@ "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", "dev": true }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -4571,6 +4694,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -4583,6 +4711,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is2": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", + "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5518,6 +5659,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -5837,8 +5997,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { "version": "1.0.0", @@ -6132,7 +6291,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6462,8 +6620,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6609,14 +6766,12 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7974,6 +8129,31 @@ "streamx": "^2.15.0" } }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8805,8 +8985,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.14.2", @@ -8920,7 +9099,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/tests/wdio/package.json b/tests/wdio/package.json index eeb0c124e..92a99ce39 100644 --- a/tests/wdio/package.json +++ b/tests/wdio/package.json @@ -30,5 +30,8 @@ }, "engines": { "node": ">=20" + }, + "dependencies": { + "chromedriver": "^120.0.1" } } diff --git a/tests/wdio/wdio.conf.ts b/tests/wdio/wdio.conf.ts index 525cfb00d..c6c16b287 100644 --- a/tests/wdio/wdio.conf.ts +++ b/tests/wdio/wdio.conf.ts @@ -61,8 +61,11 @@ export const config: Options.Testrunner = { capabilities: [ { "browserName": "chrome", + "wdio:chromedriverOptions": { + "binary": "./node_modules/.bin/chromedriver", + }, "goog:chromeOptions": { - args: ["--disable-infobars", "--window-size=1280,800"].concat( + "args": ["--disable-infobars", "--window-size=1280,800"].concat( (function () { return process.env.HEADLESS_CHROME === "1" ? [ diff --git a/web/src/admin/outposts/OutpostForm.ts b/web/src/admin/outposts/OutpostForm.ts index 1952cfe85..b24990193 100644 --- a/web/src/admin/outposts/OutpostForm.ts +++ b/web/src/admin/outposts/OutpostForm.ts @@ -1,8 +1,10 @@ +import { DataProvider, DualSelectPair } from "@goauthentik/app/elements/ak-dual-select/types"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { docLink } from "@goauthentik/common/global"; import { groupBy } from "@goauthentik/common/utils"; import "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; +import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import "@goauthentik/elements/forms/SearchSelect"; @@ -19,13 +21,68 @@ import { OutpostTypeEnum, OutpostsApi, OutpostsServiceConnectionsAllListRequest, - PaginatedLDAPProviderList, - PaginatedProxyProviderList, - PaginatedRadiusProviderList, + Pagination, ProvidersApi, ServiceConnection, } from "@goauthentik/api"; +interface ProviderBase { + pk: number; + name: string; + assignedBackchannelApplicationName?: string; + assignedApplicationName?: string; +} + +interface ProviderData { + pagination: Pagination; + results: ProviderBase[]; +} + +const api = () => new ProvidersApi(DEFAULT_CONFIG); +const args = (page: number) => ({ + ordering: "name", + applicationIsnull: false, + pageSize: 20, + search: "", + page, +}); + +const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => [ + `${item.pk}`, + `${ + item.assignedBackchannelApplicationName + ? item.assignedBackchannelApplicationName + : item.assignedApplicationName + } (${item.name})`, +]; + +const provisionMaker = (results: ProviderData) => ({ + pagination: results.pagination, + options: results.results.map(dualSelectPairMaker), +}); + +const proxyListFetch = async (page: number) => + provisionMaker(await api().providersProxyList(args(page))); + +const ldapListFetch = async (page: number) => + provisionMaker(await api().providersLdapList(args(page))); + +const radiusListFetch = async (page: number) => + provisionMaker(await api().providersRadiusList(args(page))); + +function providerProvider(type: OutpostTypeEnum): DataProvider { + switch (type) { + case OutpostTypeEnum.Proxy: + return proxyListFetch; + case OutpostTypeEnum.Ldap: + return ldapListFetch; + case OutpostTypeEnum.Radius: + return radiusListFetch; + default: + throw new Error(`Unrecognized OutputType: ${type}`); + } +} + @customElement("ak-outpost-form") export class OutpostForm extends ModelForm { @property() @@ -35,10 +92,7 @@ export class OutpostForm extends ModelForm { embedded = false; @state() - providers?: - | PaginatedProxyProviderList - | PaginatedLDAPProviderList - | PaginatedRadiusProviderList; + providers?: DataProvider; defaultConfig?: OutpostDefaultConfig; @@ -52,30 +106,9 @@ export class OutpostForm extends ModelForm { async load(): Promise { this.defaultConfig = await new OutpostsApi( - DEFAULT_CONFIG, + DEFAULT_CONFIG ).outpostsInstancesDefaultSettingsRetrieve(); - switch (this.type) { - case OutpostTypeEnum.Proxy: - this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersProxyList({ - ordering: "name", - applicationIsnull: false, - }); - break; - case OutpostTypeEnum.Ldap: - this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersLdapList({ - ordering: "name", - applicationIsnull: false, - }); - break; - case OutpostTypeEnum.Radius: - this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRadiusList({ - ordering: "name", - applicationIsnull: false, - }); - break; - case OutpostTypeEnum.UnknownDefaultOpenApi: - this.providers = undefined; - } + this.providers = providerProvider(this.type); } getSuccessMessage(): string { @@ -145,7 +178,7 @@ export class OutpostForm extends ModelForm { args.search = query; } const items = await new OutpostsApi( - DEFAULT_CONFIG, + DEFAULT_CONFIG ).outpostsServiceConnectionsAllList(args); return items.results; }} @@ -170,7 +203,7 @@ export class OutpostForm extends ModelForm {

${msg( - "Selecting an integration enables the management of the outpost by authentik.", + "Selecting an integration enables the management of the outpost by authentik." )}

@@ -185,32 +218,18 @@ export class OutpostForm extends ModelForm { ?required=${!this.embedded} name="providers" > - -

- ${msg("You can only select providers that match the type of the outpost.")} -

-

- ${msg("Hold control/command to select multiple items.")} -

+

diff --git a/web/src/elements/ak-dual-select/ak-dual-select-provider.ts b/web/src/elements/ak-dual-select/ak-dual-select-provider.ts new file mode 100644 index 000000000..7029b13d3 --- /dev/null +++ b/web/src/elements/ak-dual-select/ak-dual-select-provider.ts @@ -0,0 +1,115 @@ +import { AKElement } from "@goauthentik/elements/Base"; +import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; + +import { PropertyValues, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { createRef, ref } from "lit/directives/ref.js"; +import type { Ref } from "lit/directives/ref.js"; + +import type { Pagination } from "@goauthentik/api"; + +import "./ak-dual-select"; +import { AkDualSelect } from "./ak-dual-select"; +import type { DataProvider, DualSelectPair } from "./types"; + +/** + * @element ak-dual-select-provider + * + * A top-level component that understands how the authentik pagination interface works, + * and can provide new pages based upon navigation requests. This is the interface + * between authentik and the generic ak-dual-select component; aside from knowing that + * the Pagination object "looks like Django," the interior components don't know anything + * about authentik at all and could be dropped into Gravity unchanged.) + * + */ + +@customElement("ak-dual-select-provider") +export class AkDualSelectProvider extends CustomListenerElement(AKElement) { + // A function that takes a page and returns the DualSelectPair[] collection with which to update + // the "Available" pane. + @property({ type: Object }) + provider!: DataProvider; + + @property({ type: Array }) + selected: DualSelectPair[] = []; + + @property({ attribute: "available-label" }) + availableLabel = "Available options"; + + @property({ attribute: "selected-label" }) + selectedLabel = "Selected options"; + + @state() + private options: DualSelectPair[] = []; + + private dualSelector: Ref = createRef(); + + private isLoading = false; + + private pagination?: Pagination; + + get value() { + return this.dualSelector.value!.selected.map(([k, _]) => k); + } + + selectedMap: WeakMap = new WeakMap(); + + constructor() { + super(); + setTimeout(() => this.fetch(1), 0); + this.onNav = this.onNav.bind(this); + this.onChange = this.onChange.bind(this); + // Notify AkForElementHorizontal how to handle this thing. + this.dataset.akControl = "true"; + this.addCustomListener("ak-pagination-nav-to", this.onNav); + this.addCustomListener("ak-dual-select-change", this.onChange); + } + + onNav(event: Event) { + if (!(event instanceof CustomEvent)) { + throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`); + } + this.fetch(event.detail); + } + + onChange(event: Event) { + if (!(event instanceof CustomEvent)) { + throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); + } + this.selected = event.detail.value; + } + + willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("provider")) { + this.pagination = undefined; + if (changedProperties.get("provider")) { + this.selectedMap.set(changedProperties.get("provider"), this.selected); + this.selected = this.selectedMap.get(this.provider) ?? []; + } + this.fetch(); + } + } + + async fetch(page?: number) { + if (this.isLoading) { + return; + } + this.isLoading = true; + const goto = page ?? this.pagination?.current ?? 1; + const data = await this.provider(goto); + this.pagination = data.pagination; + this.options = data.options; + this.isLoading = false; + } + + render() { + return html``; + } +} diff --git a/web/src/elements/ak-dual-select/ak-dual-select.ts b/web/src/elements/ak-dual-select/ak-dual-select.ts index 4e81bf495..cfcbcad41 100644 --- a/web/src/elements/ak-dual-select/ak-dual-select.ts +++ b/web/src/elements/ak-dual-select/ak-dual-select.ts @@ -5,14 +5,14 @@ import { } from "@goauthentik/elements/utils/eventEmitter"; import { msg, str } from "@lit/localize"; -import { css, html, nothing } from "lit"; +import { PropertyValues, TemplateResult, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { createRef, ref } from "lit/directives/ref.js"; import type { Ref } from "lit/directives/ref.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { globalVariables, mainStyles } from "./components/styles.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import "./components/ak-dual-select-available-pane"; @@ -21,7 +21,6 @@ import "./components/ak-dual-select-controls"; import "./components/ak-dual-select-selected-pane"; import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane"; import "./components/ak-pagination"; -import { globalVariables, mainStyles } from "./components/styles.css"; import { EVENT_ADD_ALL, EVENT_ADD_ONE, @@ -33,46 +32,39 @@ import { } from "./constants"; import type { BasePagination, DualSelectPair } from "./types"; +type PairValue = string | TemplateResult; +type Pair = [string, PairValue]; +const alphaSort = ([_k1, v1]: Pair, [_k2, v2]: Pair) => (v1 < v2 ? -1 : v1 > v2 ? 1 : 0); + const styles = [ PFBase, PFButton, globalVariables, mainStyles, - css` - :host { - --pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min: 12.5rem; - --pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 28.125rem; - } - .ak-dual-list-selector { - display: grid; - grid-template-columns: - minmax( - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min), - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max) - ) - min-content - minmax( - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min), - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max) - ); - } - .ak-available-pane, - ak-dual-select-controls, - .ak-selected-pane { - height: 100%; - } - `, ]; +/** + * @element ak-dual-select + * + * A master (but independent) component that shows two lists-- one of "available options" and one of + * "selected options". The Available Options panel supports pagination if it receives a valid and + * active pagination object (based on Django's pagination object) from the invoking component. + * + * @fires ak-dual-select-change - A custom change event with the current `selected` list. + */ + @customElement("ak-dual-select") export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { static get styles() { return styles; } + /* The list of options to *currently* show. Note that this is not *all* the options, only the + * currently shown list of options from a pagination collection. */ @property({ type: Array }) options: DualSelectPair[] = []; + /* The list of options selected. This is the *entire* list and will not be paginated. */ @property({ type: Array }) selected: DualSelectPair[] = []; @@ -89,6 +81,8 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE selectedPane: Ref = createRef(); + selectedKeys: Set = new Set(); + constructor() { super(); this.handleMove = this.handleMove.bind(this); @@ -112,7 +106,21 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE return this.selected; } + willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("selected")) { + this.selectedKeys = new Set(this.selected.map(([key, _]) => key)); + } + // Pagination invalidates available moveables. + if (changedProperties.has("options") && this.availablePane.value) { + this.availablePane.value.clearMove(); + } + } + handleMove(eventName: string, event: Event) { + if (!(event instanceof CustomEvent)) { + throw new Error(`Expected move event here, got ${eventName}`); + } + switch (eventName) { case EVENT_ADD_SELECTED: { this.addSelected(); @@ -148,7 +156,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE `AkDualSelect.handleMove received unknown event type: ${eventName}` ); } - this.dispatchCustomEvent("change", { value: this.selected }); + this.dispatchCustomEvent("ak-dual-select-change", { value: this.value }); event.stopPropagation(); } @@ -175,13 +183,10 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE } } - removeOne(key: string) { - this.selected = this.selected.filter(([k, _]) => k !== key); - } - - // You must remember, these are the *currently visible* options; the parent node is responsible - // for paginating and updating the list of currently visible options; + // These are the *currently visible* options; the parent node is responsible for paginating and + // updating the list of currently visible options; addAllVisible() { + // Create a new array of all current options and selected, and de-dupe. const selected = new Map([...this.options, ...this.selected]); this.selected = Array.from(selected.entries()).sort(); this.availablePane.value!.clearMove(); @@ -196,8 +201,12 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE this.selectedPane.value!.clearMove(); } - // Remove all the items from selected that are in the *currently visible* options list + removeOne(key: string) { + this.selected = this.selected.filter(([k, _]) => k !== key); + } + removeAllVisible() { + // Remove all the items from selected that are in the *currently visible* options list const options = new Set(this.options.map(([k, _]) => k)); this.selected = this.selected.filter(([k, _]) => !options.has(k)); this.selectedPane.value!.clearMove(); @@ -208,21 +217,21 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE this.selectedPane.value!.clearMove(); } - selectedKeys() { - return new Set(this.selected.map(([k, _]) => k)); - } - get canAddAll() { // False unless any visible option cannot be found in the selected list, so can still be // added. - const selected = this.selectedKeys(); - return this.options.length > 0 && !!this.options.find(([key, _]) => !selected.has(key)); + const allMoved = + this.options.length === + this.options.filter(([key, _]) => this.selectedKeys.has(key)).length; + + return this.options.length > 0 && !allMoved; } get canRemoveAll() { // False if no visible option can be found in the selected list - const selected = this.selectedKeys(); - return this.options.length > 0 && !!this.options.find(([key, _]) => selected.has(key)); + return ( + this.options.length > 0 && !!this.options.find(([key, _]) => this.selectedKeys.has(key)) + ); } get needPagination() { @@ -230,7 +239,6 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE } render() { - const selected = this.selectedKeys(); const availableCount = this.availablePane.value?.toMove.size ?? 0; const selectedCount = this.selectedPane.value?.toMove.size ?? 0; const selectedTotal = this.selected.length; @@ -262,7 +270,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE ${this.needPagination ? html`` @@ -296,7 +304,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE diff --git a/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts b/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts index 7441a8369..d0780d2ff 100644 --- a/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts +++ b/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts @@ -1,7 +1,7 @@ import { AKElement } from "@goauthentik/elements/Base"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; -import { css, html, nothing } from "lit"; +import { PropertyValues, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { map } from "lit/directives/map.js"; @@ -12,26 +12,14 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { EVENT_ADD_ONE } from "../constants"; import type { DualSelectPair } from "../types"; +import { availablePaneStyles, listStyles } from "./styles.css"; const styles = [ PFBase, PFButton, PFDualListSelector, - css` - .pf-c-dual-list-selector__item { - padding: 0.25rem; - } - .pf-c-dual-list-selector__item-text i { - display: inline-block; - margin-left: 0.5rem; - font-weight: 200; - color: var(--pf-global--palette--black-500); - font-size: var(--pf-global--FontSize--xs); -} -.pf-c-dual-list-selector__menu { -width: 1fr; -} - `, + listStyles, + availablePaneStyles ]; const hostAttributes = [ @@ -48,11 +36,13 @@ const hostAttributes = [ * of objects selected to move. "selected" options are marked with a checkmark to show they're * already in the "selected" collection and would be pointless to move. * - * @fires ak-dual-select-available-move-changed - When the list of "to move" entries changed. Includes the current * `toMove` content. + * @fires ak-dual-select-available-move-changed - When the list of "to move" entries changed. + * Includes the current * `toMove` content. + * * @fires ak-dual-select-add-one - Doubleclick with the element clicked on. * - * It is not expected that the `ak-dual-select-available-move-changed` will be used; instead, the - * attribute will be read by the parent when a control is clicked. + * It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead, + * the attribute will be read by the parent when a control is clicked. * */ @customElement("ak-dual-select-available-pane") @@ -65,7 +55,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { @property({ type: Array }) readonly options: DualSelectPair[] = []; - /* An set (set being easy for lookups) of keys with all the pairs selected, so that the ones + /* A set (set being easy for lookups) of keys with all the pairs selected, so that the ones * currently being shown that have already been selected can be marked and their clicks ignored. * */ @@ -87,6 +77,15 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { this.onMove = this.onMove.bind(this); } + connectedCallback() { + super.connectedCallback(); + hostAttributes.forEach(([attr, value]) => { + if (!this.hasAttribute(attr)) { + this.setAttribute(attr, value); + } + }); + } + get moveable() { return Array.from(this.toMove.values()); } @@ -97,8 +96,6 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { onClick(key: string) { if (this.selected.has(key)) { - // An already selected item cannot be moved into the "selected" category - console.warn(`Attempted to mark '${key}' when it should have been unavailable`); return; } if (this.toMove.has(key)) { @@ -112,7 +109,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { ); this.dispatchCustomEvent("ak-dual-select-move"); // Necessary because updating a map won't trigger a state change - this.requestUpdate(); + this.requestUpdate(); } onMove(key: string) { @@ -121,15 +118,6 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { this.requestUpdate(); } - connectedCallback() { - super.connectedCallback(); - hostAttributes.forEach(([attr, value]) => { - if (!this.hasAttribute(attr)) { - this.setAttribute(attr, value); - } - }); - } - // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and // will not re-arrange or reconstruct the list automatically if the actual sources do not // change; this allows the available pane to illustrate selected items with the checkmark @@ -137,35 +125,36 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { render() { return html` -

-
    - ${map(this.options, ([key, label]) => { - const selected = classMap({ - "pf-m-selected": this.toMove.has(key), - }); - return html`
  • this.onClick(key)} - @dblclick=${() => this.onMove(key)} - role="option" - tabindex="-1" - > -
    - - - ${label}${this.selected.has(key) - ? html`` - : nothing} +
      + ${map(this.options, ([key, label]) => { + const selected = classMap({ + "pf-m-selected": this.toMove.has(key), + }); + return html`
    • this.onClick(key)} + @dblclick=${() => this.onMove(key)} + role="option" + data-ak-key=${key} + tabindex="-1" + > +
      + + + ${label}${this.selected.has(key) + ? html`` + : nothing} -
      -
    • `; - })} -
    -
    + > +
+ `; + })} + + `; } } diff --git a/web/src/elements/ak-dual-select/components/ak-dual-select-controls.ts b/web/src/elements/ak-dual-select/components/ak-dual-select-controls.ts index b92cf86c8..c8c88ac8d 100644 --- a/web/src/elements/ak-dual-select/components/ak-dual-select-controls.ts +++ b/web/src/elements/ak-dual-select/components/ak-dual-select-controls.ts @@ -27,13 +27,13 @@ const styles = [ } .pf-c-dual-list-selector { max-width: 4rem; -} -.ak-dual-list-selector__controls { -display: grid; -justify-content: center; -align-content: center; -height: 100%; -} + } + .ak-dual-list-selector__controls { + display: grid; + justify-content: center; + align-content: center; + height: 100%; + } `, ]; @@ -104,8 +104,8 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) { return html`
+ `; + })} + + `; } } diff --git a/web/src/elements/ak-dual-select/components/ak-pagination.ts b/web/src/elements/ak-dual-select/components/ak-pagination.ts index 4a99a8325..25b08e859 100644 --- a/web/src/elements/ak-dual-select/components/ak-pagination.ts +++ b/web/src/elements/ak-dual-select/components/ak-pagination.ts @@ -1,4 +1,5 @@ import { AKElement } from "@goauthentik/elements/Base"; +import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { msg, str } from "@lit/localize"; import { css, html, nothing } from "lit"; @@ -8,7 +9,6 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import type { BasePagination } from "../types"; const styles = [ @@ -54,7 +54,7 @@ export class AkPagination extends CustomEmitterElement(AKElement) {
${msg( - str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`, + str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}` )}
diff --git a/web/src/elements/ak-dual-select/components/styles.css.ts b/web/src/elements/ak-dual-select/components/styles.css.ts index c6626ac5c..60a2b3c8c 100644 --- a/web/src/elements/ak-dual-select/components/styles.css.ts +++ b/web/src/elements/ak-dual-select/components/styles.css.ts @@ -1,5 +1,10 @@ import { css } from "lit"; +// The `host` information for the Patternfly dual list selector came with some default settings that +// we do not want in a web component. By isolating what we *really* use into this collection here, +// we get all the benefits of Patternfly without having to wrestle without also having to counteract +// those default settings. + export const globalVariables = css` :host { --pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min: 12.5rem; @@ -85,6 +90,14 @@ export const globalVariables = css` `; export const mainStyles = css` + :host { + --pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min: 12.5rem; + --pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 28.125rem; + } + :host { + display: block grid; + } + .pf-c-dual-list-selector__title-text { font-weight: var(--pf-c-dual-list-selector__title-text--FontWeight); } @@ -96,27 +109,64 @@ export const mainStyles = css` .ak-dual-list-selector { display: grid; - grid-template-columns: - minmax( - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min), - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max) - ) - min-content - minmax( - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min), - var(--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max) - ); + grid-template-columns: minmax(0, 1fr) min-content minmax(0, 1fr); + } + + .ak-available-pane, + .ak-selected-pane { + display: grid; + grid-template-rows: auto auto 1fr auto; + max-width: 100%; + overflow: hidden; + } + + ak-dual-select-controls { + height: 100%; } `; -export const selectedPaneStyles = css` -.pf-c-dual-list-selector__menu { -height: 100%; -} -.pf-c-dual-list-selector__item { -padding: 0.25rem; -} -input[type="checkbox"][readonly] { -pointer-events: none; -} +export const listStyles = css` + :host { + display: block; + overflow: hidden; + max-width: 100%; + } + + .pf-c-dual-list-selector__menu { + max-width: 100%; + height: 100%; + } + + .pf-c-dual-list-selector__list { + max-width: 100%; + display: block; + } + + .pf-c-dual-list-selector__item { + padding: 0.25rem; + width: auto; + } + + .pf-c-dual-list-selector__item-text { + user-select: none; + flex-grow: 0; + } + +`; + + +export const selectedPaneStyles = css` + input[type="checkbox"][readonly] { + pointer-events: none; + } +`; + +export const availablePaneStyles = css` + .pf-c-dual-list-selector__item-text i { + display: inline-block; + margin-left: 0.5rem; + font-weight: 200; + color: var(--pf-global--palette--black-500); + font-size: var(--pf-global--FontSize--xs); + } `; diff --git a/web/src/elements/ak-dual-select/index.ts b/web/src/elements/ak-dual-select/index.ts index cd8572289..134aef93a 100644 --- a/web/src/elements/ak-dual-select/index.ts +++ b/web/src/elements/ak-dual-select/index.ts @@ -1,5 +1,8 @@ import { AkDualSelect } from "./ak-dual-select"; import "./ak-dual-select"; -export { AkDualSelect } +import { AkDualSelectProvider } from "./ak-dual-select-provider"; +import "./ak-dual-select-provider"; + +export { AkDualSelect, AkDualSelectProvider } export default AkDualSelect; diff --git a/web/src/elements/ak-dual-select/types.ts b/web/src/elements/ak-dual-select/types.ts index 65a8d090d..a80c7983d 100644 --- a/web/src/elements/ak-dual-select/types.ts +++ b/web/src/elements/ak-dual-select/types.ts @@ -8,3 +8,10 @@ export type BasePagination = Pick< Pagination, "startIndex" | "endIndex" | "count" | "previous" | "next" >; + +export type DataProvision = { + pagination: Pagination; + options: DualSelectPair[]; +} + +export type DataProvider = (page: number) => Promise; diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index d465a2435..5d6994202 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -69,7 +69,6 @@ export function serializeForm( return; } - // TODO: Tighten up the typing so that we can handle both. if ("akControl" in element.dataset) { assignValue(element, (element as unknown as AkControlElement).json(), json); return; @@ -79,6 +78,12 @@ export function serializeForm( if (element.hidden || !inputElement) { return; } + + if ("akControl" in inputElement.dataset) { + assignValue(element, inputElement.value, json); + return; + } + // Skip elements that are writeOnly where the user hasn't clicked on the value if (element.writeOnly && !element.writeOnlyActivated) { return; diff --git a/web/src/elements/forms/HorizontalFormElement.ts b/web/src/elements/forms/HorizontalFormElement.ts index 9d00327da..c5973d9f1 100644 --- a/web/src/elements/forms/HorizontalFormElement.ts +++ b/web/src/elements/forms/HorizontalFormElement.ts @@ -36,6 +36,22 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; * */ +const isAkControl = (el: unknown): boolean => + el instanceof HTMLElement && + "dataset" in el && + el.dataset instanceof DOMStringMap && + "akControl" in el.dataset; + +const nameables = new Set([ + "input", + "textarea", + "select", + "ak-codemirror", + "ak-chip-group", + "ak-search-select", + "ak-radio", +]); + @customElement("ak-form-element-horizontal") export class HorizontalFormElement extends AKElement { static get styles(): CSSResult[] { @@ -112,19 +128,18 @@ export class HorizontalFormElement extends AKElement { }); } this.querySelectorAll("*").forEach((input) => { - switch (input.tagName.toLowerCase()) { - case "input": - case "textarea": - case "select": - case "ak-codemirror": - case "ak-chip-group": - case "ak-search-select": - case "ak-radio": - input.setAttribute("name", this.name); - break; - default: - return; + if (isAkControl(input) && !input.getAttribute("name")) { + input.setAttribute("name", this.name); + // This is fine; writeOnly won't apply to anything built this way. + return; } + + if (nameables.has(input.tagName.toLowerCase())) { + input.setAttribute("name", this.name); + } else { + return; + } + if (this.writeOnly && !this.writeOnlyActivated) { const i = input as HTMLInputElement; i.setAttribute("hidden", "true");