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. :-)
This commit is contained in:
parent
cef378da82
commit
7c4fafdc28
|
@ -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"
|
||||
|
|
|
@ -30,5 +30,8 @@
|
|||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"chromedriver": "^120.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
? [
|
||||
|
|
|
@ -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<Outpost, string> {
|
||||
@property()
|
||||
|
@ -35,10 +92,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||
embedded = false;
|
||||
|
||||
@state()
|
||||
providers?:
|
||||
| PaginatedProxyProviderList
|
||||
| PaginatedLDAPProviderList
|
||||
| PaginatedRadiusProviderList;
|
||||
providers?: DataProvider;
|
||||
|
||||
defaultConfig?: OutpostDefaultConfig;
|
||||
|
||||
|
@ -52,30 +106,9 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||
|
||||
async load(): Promise<void> {
|
||||
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<Outpost, string> {
|
|||
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<Outpost, string> {
|
|||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Selecting an integration enables the management of the outpost by authentik.",
|
||||
"Selecting an integration enables the management of the outpost by authentik."
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
@ -185,32 +218,18 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||
?required=${!this.embedded}
|
||||
name="providers"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.providers?.results.map((provider) => {
|
||||
const selected = Array.from(this.instance?.providers || []).some((sp) => {
|
||||
return sp == provider.pk;
|
||||
});
|
||||
let appName = provider.assignedApplicationName;
|
||||
if (provider.assignedBackchannelApplicationName) {
|
||||
appName = provider.assignedBackchannelApplicationName;
|
||||
}
|
||||
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
|
||||
${appName} (${provider.name})
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("You can only select providers that match the type of the outpost.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
<ak-dual-select-provider
|
||||
.provider=${this.providers}
|
||||
.selected=${(this.instance?.providersObj ?? []).map(dualSelectPairMaker)}
|
||||
available-label="Available Applications"
|
||||
selected-label="Selected Applications"
|
||||
></ak-dual-select-provider>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(
|
||||
this.instance ? this.instance.config : this.defaultConfig?.config,
|
||||
this.instance ? this.instance.config : this.defaultConfig?.config
|
||||
)}"
|
||||
></ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
|
|
@ -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<AkDualSelect> = createRef();
|
||||
|
||||
private isLoading = false;
|
||||
|
||||
private pagination?: Pagination;
|
||||
|
||||
get value() {
|
||||
return this.dualSelector.value!.selected.map(([k, _]) => k);
|
||||
}
|
||||
|
||||
selectedMap: WeakMap<DataProvider, DualSelectPair[]> = 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<this>) {
|
||||
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`<ak-dual-select
|
||||
${ref(this.dualSelector)}
|
||||
.options=${this.options}
|
||||
.pages=${this.pagination}
|
||||
.selected=${this.selected}
|
||||
available-label=${this.availableLabel}
|
||||
selected-label=${this.selectedLabel}
|
||||
></ak-dual-select>`;
|
||||
}
|
||||
}
|
|
@ -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<AkDualSelectSelectedPane> = createRef();
|
||||
|
||||
selectedKeys: Set<string> = 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<this>) {
|
||||
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
|
|||
<ak-dual-select-available-pane
|
||||
${ref(this.availablePane)}
|
||||
.options=${this.options}
|
||||
.selected=${selected}
|
||||
.selected=${this.selectedKeys}
|
||||
></ak-dual-select-available-pane>
|
||||
${this.needPagination
|
||||
? html`<ak-pagination .pages=${this.pages}></ak-pagination>`
|
||||
|
@ -296,7 +304,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
|||
|
||||
<ak-dual-select-selected-pane
|
||||
${ref(this.selectedPane)}
|
||||
.selected=${this.selected}
|
||||
.selected=${this.selected.toSorted(alphaSort)}
|
||||
></ak-dual-select-selected-pane>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -149,6 +137,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
|
|||
@click=${() => this.onClick(key)}
|
||||
@dblclick=${() => this.onMove(key)}
|
||||
role="option"
|
||||
data-ak-key=${key}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="pf-c-dual-list-selector__list-item-row ${selected}">
|
||||
|
|
|
@ -104,8 +104,8 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
|
|||
return html`
|
||||
<div class="pf-c-dual-list-selector__controls-item">
|
||||
<button
|
||||
?aria-disabled=${this.disabled || !active}
|
||||
?disabled=${this.disabled || !active}
|
||||
?aria-disabled=${!active}
|
||||
?disabled=${!active}
|
||||
aria-label=${label}
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
|
|
|
@ -10,17 +10,11 @@ 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 type { DualSelectPair } from "../types";
|
||||
import { EVENT_REMOVE_ONE } from "../constants";
|
||||
import { selectedPaneStyles } from "./styles.css";
|
||||
|
||||
const styles = [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFDualListSelector,
|
||||
selectedPaneStyles
|
||||
];
|
||||
import type { DualSelectPair } from "../types";
|
||||
import { listStyles, selectedPaneStyles } from "./styles.css";
|
||||
|
||||
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
|
||||
|
||||
const hostAttributes = [
|
||||
["aria-labelledby", "dual-list-selector-selected-pane-status"],
|
||||
|
@ -34,7 +28,9 @@ const hostAttributes = [
|
|||
* The "selected options" or "right" pane in a dual-list multi-select. It receives from its parent
|
||||
* a list of the selected options, and maintains an internal list of objects selected to move.
|
||||
*
|
||||
* @fires ak-dual-select-selected-move-changed - When the list of "to move" entries changed. Includes the current * `toMove` content.
|
||||
* @fires ak-dual-select-selected-move-changed - When the list of "to move" entries changed.
|
||||
* Includes the current * `toMove` content.
|
||||
*
|
||||
* @fires ak-dual-select-remove-one - Doubleclick with the element clicked on.
|
||||
*
|
||||
* It is not expected that the `ak-dual-select-selected-move-changed` will be used; instead, the
|
||||
|
@ -120,6 +116,7 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
|
|||
@click=${() => this.onClick(key)}
|
||||
@dblclick=${() => this.onMove(key)}
|
||||
role="option"
|
||||
data-ak-key=${key}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="pf-c-dual-list-selector__list-item-row ${selected}">
|
||||
|
|
|
@ -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) {
|
|||
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
|
||||
<span class="pf-c-options-menu__toggle-text">
|
||||
${msg(
|
||||
str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`,
|
||||
str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -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`
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<DataProvision>;
|
||||
|
|
|
@ -69,7 +69,6 @@ export function serializeForm<T extends KeyUnknown>(
|
|||
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<T extends KeyUnknown>(
|
|||
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;
|
||||
|
|
|
@ -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":
|
||||
if (isAkControl(input) && !input.getAttribute("name")) {
|
||||
input.setAttribute("name", this.name);
|
||||
break;
|
||||
default:
|
||||
// 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");
|
||||
|
|
Reference in New Issue