mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge branch 'main' into ac/pm-17168/revoking-user-is-not-synced-immediately-via-push-notification
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -97,12 +97,15 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
|
||||
.github/workflows/scan.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
|
||||
# ESLint custom rules
|
||||
libs/eslint @bitwarden/team-platform-dev
|
||||
|
||||
## Autofill team files ##
|
||||
apps/browser/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/autofill @bitwarden/team-autofill-dev
|
||||
libs/common/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
|
||||
# DuckDuckGo integration
|
||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev
|
||||
|
||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -211,6 +211,8 @@
|
||||
"@storybook/angular",
|
||||
"@storybook/manager-api",
|
||||
"@storybook/theming",
|
||||
"@typescript-eslint/utils",
|
||||
"@typescript-eslint/rule-tester",
|
||||
"@types/react",
|
||||
"autoprefixer",
|
||||
"bootstrap",
|
||||
|
||||
@@ -982,8 +982,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
const queueLength = this.mutationsQueue.length;
|
||||
|
||||
if (!this.domQueryService.pageContainsShadowDomElements()) {
|
||||
// Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop.
|
||||
requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 });
|
||||
this.checkPageContainsShadowDom();
|
||||
}
|
||||
|
||||
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
|
||||
|
||||
1
apps/desktop/desktop_native/.gitignore
vendored
1
apps/desktop/desktop_native/.gitignore
vendored
@@ -5,3 +5,4 @@ index.node
|
||||
npm-debug.log*
|
||||
*.node
|
||||
dist
|
||||
windows_pluginauthenticator_bindings.rs
|
||||
|
||||
72
apps/desktop/desktop_native/Cargo.lock
generated
72
apps/desktop/desktop_native/Cargo.lock
generated
@@ -410,6 +410,26 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
@@ -553,6 +573,15 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -593,6 +622,17 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.27"
|
||||
@@ -1458,6 +1498,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@@ -2259,6 +2308,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
@@ -2437,6 +2496,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3447,6 +3512,13 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-plugin-authenticator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["napi", "core", "proxy", "macos_provider"]
|
||||
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "windows-plugin-authenticator"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
publish = false
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
bindgen = "0.71.1"
|
||||
@@ -0,0 +1,23 @@
|
||||
# windows-plugin-authenticator
|
||||
|
||||
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
|
||||
|
||||
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
|
||||
|
||||
## Building
|
||||
|
||||
To build this crate, set the following environment variables:
|
||||
|
||||
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
|
||||
|
||||
### Bash Example
|
||||
|
||||
```
|
||||
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
|
||||
### PowerShell Example
|
||||
|
||||
```
|
||||
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows() {
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("pluginauthenticator.hpp")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings.");
|
||||
|
||||
bindings
|
||||
.write_to_file(format!(
|
||||
"{}\\windows_pluginauthenticator_bindings.rs",
|
||||
out_dir
|
||||
))
|
||||
.expect("Couldn't write bindings.");
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Bitwarden's pluginauthenticator.hpp
|
||||
|
||||
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
|
||||
|
||||
This is a C++ header file, so the extension has been manually
|
||||
changed from `.h` to `.hpp`, so bindgen will automatically
|
||||
generate the correct C++ bindings.
|
||||
|
||||
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
|
||||
*/
|
||||
|
||||
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
|
||||
|
||||
/* File created by MIDL compiler version 8.01.0628 */
|
||||
/* @@MIDL_FILE_HEADING( ) */
|
||||
|
||||
/* verify that the <rpcndr.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCNDR_H_VERSION__
|
||||
#define __REQUIRED_RPCNDR_H_VERSION__ 501
|
||||
#endif
|
||||
|
||||
/* verify that the <rpcsal.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCSAL_H_VERSION__
|
||||
#define __REQUIRED_RPCSAL_H_VERSION__ 100
|
||||
#endif
|
||||
|
||||
#include "rpc.h"
|
||||
#include "rpcndr.h"
|
||||
|
||||
#ifndef __RPCNDR_H_VERSION__
|
||||
#error this stub requires an updated version of <rpcndr.h>
|
||||
#endif /* __RPCNDR_H_VERSION__ */
|
||||
|
||||
#ifndef COM_NO_WINDOWS_H
|
||||
#include "windows.h"
|
||||
#include "ole2.h"
|
||||
#endif /*COM_NO_WINDOWS_H*/
|
||||
|
||||
#ifndef __pluginauthenticator_h__
|
||||
#define __pluginauthenticator_h__
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
#ifndef DECLSPEC_XFGVIRT
|
||||
#if defined(_CONTROL_FLOW_GUARD_XFG)
|
||||
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
|
||||
#else
|
||||
#define DECLSPEC_XFGVIRT(base, func)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Forward Declarations */
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
|
||||
|
||||
/* header files for imported files */
|
||||
#include "oaidl.h"
|
||||
#include "webauthn.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
|
||||
/* [local] */
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
{
|
||||
HWND hWnd;
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
DWORD cbEncodedRequest;
|
||||
/* [size_is] */ byte *pbEncodedRequest;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
|
||||
{
|
||||
DWORD cbEncodedResponse;
|
||||
/* [size_is] */ byte *pbEncodedResponse;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
|
||||
{
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
|
||||
/* interface EXPERIMENTAL_IPluginAuthenticator */
|
||||
/* [unique][version][uuid][object] */
|
||||
|
||||
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#if defined(__cplusplus) && !defined(CINTERFACE)
|
||||
|
||||
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
|
||||
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
|
||||
|
||||
};
|
||||
|
||||
#else /* C style interface */
|
||||
|
||||
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
|
||||
{
|
||||
BEGIN_INTERFACE
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
|
||||
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in REFIID riid,
|
||||
/* [annotation][iid_is][out] */
|
||||
_COM_Outptr_ void **ppvObject);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, AddRef)
|
||||
ULONG ( STDMETHODCALLTYPE *AddRef )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, Release)
|
||||
ULONG ( STDMETHODCALLTYPE *Release )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
|
||||
|
||||
END_INTERFACE
|
||||
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
|
||||
|
||||
interface EXPERIMENTAL_IPluginAuthenticator
|
||||
{
|
||||
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
#ifdef COBJMACROS
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
|
||||
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
|
||||
( (This)->lpVtbl -> AddRef(This) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
|
||||
( (This)->lpVtbl -> Release(This) )
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
|
||||
|
||||
#endif /* COBJMACROS */
|
||||
|
||||
#endif /* C style interface */
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
|
||||
|
||||
/* Additional Prototypes for ALL interfaces */
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
/* end of Additional Prototypes */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod pa;
|
||||
|
||||
pub fn get_version_number() -> u64 {
|
||||
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
|
||||
}
|
||||
|
||||
pub fn add_authenticator() {
|
||||
unimplemented!();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
The 'pa' (plugin authenticator) module will contain the generated
|
||||
bindgen code.
|
||||
|
||||
The attributes below will suppress warnings from the generated code.
|
||||
*/
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(clippy::all)]
|
||||
#![allow(warnings)]
|
||||
|
||||
include!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/windows_pluginauthenticator_bindings.rs"
|
||||
));
|
||||
@@ -1,158 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="tw-container tw-mx-auto"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div>
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_email"
|
||||
bitInput
|
||||
type="email"
|
||||
formControlName="email"
|
||||
[attr.readonly]="queryParamFromOrgInvite ? true : null"
|
||||
/>
|
||||
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input id="register-form_input_name" bitInput type="text" formControlName="name" />
|
||||
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
|
||||
</auth-password-callout>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
<bit-hint>
|
||||
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
|
||||
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<app-password-strength
|
||||
[password]="formGroup.get('masterPassword')?.value"
|
||||
[email]="formGroup.get('email')?.value"
|
||||
[name]="formGroup.get('name')?.value"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_confirm-master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmMasterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input id="register-form_input_hint" bitInput type="text" formControlName="hint" />
|
||||
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="tw-mb-4 tw-flex tw-items-start">
|
||||
<input
|
||||
class="mt-1"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
id="checkForBreaches"
|
||||
name="CheckBreach"
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label for="checkForBreaches"> {{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</div>
|
||||
<div class="tw-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
|
||||
<input
|
||||
class="mt-1"
|
||||
id="register-form-input-accept-policies"
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="acceptPolicies"
|
||||
/>
|
||||
|
||||
<bit-label for="register-form-input-accept-policies">
|
||||
{{ "acceptPolicies" | i18n }}<br />
|
||||
<a bitLink href="https://bitwarden.com/terms/" target="_blank" rel="noreferrer">{{
|
||||
"termsOfService" | i18n
|
||||
}}</a
|
||||
>,
|
||||
<a bitLink href="https://bitwarden.com/privacy/" target="_blank" rel="noreferrer">{{
|
||||
"privacyPolicy" | i18n
|
||||
}}</a>
|
||||
</bit-label>
|
||||
</div>
|
||||
|
||||
<div class="tw-space-x-2 tw-pt-2">
|
||||
<ng-container *ngIf="!accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<p class="tw-m-0 tw-mt-5 tw-text-sm">
|
||||
{{ "alreadyHaveAccount" | i18n }}
|
||||
<a bitLink routerLink="/login">{{ "logIn" | i18n }}</a>
|
||||
</p>
|
||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,115 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-register-form",
|
||||
templateUrl: "./register-form.component.html",
|
||||
})
|
||||
export class RegisterFormComponent extends BaseRegisterComponent implements OnInit {
|
||||
@Input() queryParamEmail: string;
|
||||
@Input() queryParamFromOrgInvite: boolean;
|
||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
@Input() referenceDataValue: ReferenceEventRequest;
|
||||
|
||||
showErrorSummary = false;
|
||||
characterMinimumMessage: string;
|
||||
|
||||
constructor(
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
auditService: AuditService,
|
||||
dialogService: DialogService,
|
||||
acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
formValidationErrorService,
|
||||
formBuilder,
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
keyService,
|
||||
apiService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
dialogService,
|
||||
toastService,
|
||||
);
|
||||
this.modifyRegisterRequest = async (request: RegisterRequest) => {
|
||||
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
||||
// Org user id and token are included here only for validation and two factor purposes.
|
||||
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
request.organizationUserId = orgInvite.organizationUserId;
|
||||
request.token = orgInvite.token;
|
||||
}
|
||||
// Invite is accepted after login (on deep link redirect).
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.referenceData = this.referenceDataValue;
|
||||
if (this.queryParamEmail) {
|
||||
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
||||
}
|
||||
|
||||
if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) {
|
||||
this.characterMinimumMessage = "";
|
||||
} else {
|
||||
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthResult.score,
|
||||
this.formGroup.value.masterPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit(false);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { RegisterFormComponent } from "./register-form.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, PasswordCalloutComponent],
|
||||
declarations: [RegisterFormComponent],
|
||||
exports: [RegisterFormComponent],
|
||||
})
|
||||
export class RegisterFormModule {}
|
||||
@@ -1045,10 +1045,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
this.estimatedTax = invoice.taxAmount;
|
||||
})
|
||||
.catch((error) => {
|
||||
const translatedMessage = this.i18nService.t(error.message);
|
||||
this.toastService.showToast({
|
||||
title: "",
|
||||
variant: "error",
|
||||
message: this.i18nService.t(error.message),
|
||||
message:
|
||||
!translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,6 +294,10 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #setupSelfHost>
|
||||
<ng-container *ngIf="userOrg.hasReseller && resellerSeatsRemainingMessage">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">{{ "manageSubscription" | i18n }}</h2>
|
||||
<p bitTypography="body1">{{ resellerSeatsRemainingMessage }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showSelfHost">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">
|
||||
{{ "selfHostingTitleProper" | i18n }}
|
||||
|
||||
@@ -4,13 +4,17 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
OrganizationApiKeyType,
|
||||
OrganizationUserStatusType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -61,12 +65,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
showSubscription = true;
|
||||
showSelfHost = false;
|
||||
organizationIsManagedByConsolidatedBillingMSP = false;
|
||||
resellerSeatsRemainingMessage: string;
|
||||
|
||||
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
|
||||
protected readonly teamsStarter = ProductTierType.TeamsStarter;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private seatsRemainingMessage: string;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
@@ -79,6 +86,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -104,6 +112,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.userOrg.hasReseller) {
|
||||
const allUsers = await this.organizationUserApiService.getAllUsers(this.userOrg.id);
|
||||
|
||||
const userCount = allUsers.data.filter((user) =>
|
||||
[
|
||||
OrganizationUserStatusType.Invited,
|
||||
OrganizationUserStatusType.Accepted,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
].includes(user.status),
|
||||
).length;
|
||||
|
||||
const remainingSeats = this.userOrg.seats - userCount;
|
||||
|
||||
const seatsRemaining = this.i18nService.t(
|
||||
"seatsRemaining",
|
||||
remainingSeats.toString(),
|
||||
this.userOrg.seats.toString(),
|
||||
);
|
||||
|
||||
this.resellerSeatsRemainingMessage = seatsRemaining;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
<app-vertical-stepper #stepper linear>
|
||||
<app-vertical-step
|
||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
||||
[editable]="false"
|
||||
[subLabel]="subLabels.createAccount"
|
||||
[addSubLabelSpacing]="true"
|
||||
>
|
||||
<app-register-form
|
||||
[referenceDataValue]="referenceEventRequest"
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="accountCreated($event)"
|
||||
>
|
||||
</app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||
[subLabel]="subLabels.organizationInfo"
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
<app-vertical-stepper #stepper linear>
|
||||
<app-vertical-step
|
||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
||||
[editable]="false"
|
||||
[subLabel]="createAccountLabel"
|
||||
[addSubLabelSpacing]="true"
|
||||
>
|
||||
<app-register-form
|
||||
[referenceDataValue]="referenceEventRequest"
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="accountCreated($event)"
|
||||
>
|
||||
</app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||
[subLabel]="subLabels.organizationInfo"
|
||||
|
||||
@@ -19,7 +19,16 @@ import {
|
||||
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
||||
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||
import { ValidOrgParams } from "../trial-initiation.component";
|
||||
|
||||
export enum ValidOrgParams {
|
||||
families = "families",
|
||||
enterprise = "enterprise",
|
||||
teams = "teams",
|
||||
teamsStarter = "teamsStarter",
|
||||
individual = "individual",
|
||||
premium = "premium",
|
||||
free = "free",
|
||||
}
|
||||
|
||||
const trialFlowOrgs = [
|
||||
ValidOrgParams.teams,
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<app-secrets-manager-trial
|
||||
*ngIf="layout === layouts.secretsManager; else passwordManagerTrial"
|
||||
></app-secrets-manager-trial>
|
||||
<ng-template #passwordManagerTrial>
|
||||
<div *ngIf="accountCreateOnly" class="">
|
||||
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-min-w-xl tw-m-auto tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-register-form
|
||||
[queryParamEmail]="email"
|
||||
[queryParamFromOrgInvite]="fromOrgInvite"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!accountCreateOnly">
|
||||
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
|
||||
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
|
||||
<div class="tw-w-1/2">
|
||||
<img
|
||||
alt="Bitwarden"
|
||||
style="height: 50px; width: 335px"
|
||||
class="tw-mt-6"
|
||||
src="../../images/register-layout/logo-horizontal-white.svg"
|
||||
/>
|
||||
|
||||
<div class="tw-pt-12">
|
||||
<!-- Layout params are used by marketing to determine left-hand content -->
|
||||
<app-default-content *ngIf="layout === layouts.default"></app-default-content>
|
||||
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
|
||||
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
|
||||
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
|
||||
<app-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-content>
|
||||
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
|
||||
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
|
||||
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
|
||||
<app-cnet-enterprise-content
|
||||
*ngIf="layout === layouts.cnetcmpgnent"
|
||||
></app-cnet-enterprise-content>
|
||||
<app-cnet-individual-content
|
||||
*ngIf="layout === layouts.cnetcmpgnind"
|
||||
></app-cnet-individual-content>
|
||||
<app-cnet-teams-content
|
||||
*ngIf="layout === layouts.cnetcmpgnteams"
|
||||
></app-cnet-teams-content>
|
||||
<app-abm-enterprise-content
|
||||
*ngIf="layout === layouts.abmenterprise"
|
||||
></app-abm-enterprise-content>
|
||||
<app-abm-teams-content *ngIf="layout === layouts.abmteams"></app-abm-teams-content>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-w-1/2">
|
||||
<div *ngIf="!useTrialStepper">
|
||||
<div
|
||||
class="tw-min-w-xl tw-m-auto tw-mt-28 tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-register-form
|
||||
[queryParamEmail]="email"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-pt-44" *ngIf="useTrialStepper">
|
||||
<div
|
||||
class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background"
|
||||
>
|
||||
<div class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
||||
{{ freeTrialText }}
|
||||
</h2>
|
||||
<environment-selector
|
||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
||||
></environment-selector>
|
||||
</div>
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<app-register-form
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="createdAccount($event)"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="orgInfoFormGroup.get('name').invalid"
|
||||
[loading]="loading"
|
||||
(click)="createOrganizationOnTrial()"
|
||||
>
|
||||
{{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
||||
</button>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="Billing"
|
||||
[subLabel]="billingSubLabel"
|
||||
*ngIf="!(enableTrialPayment$ | async)"
|
||||
>
|
||||
<app-trial-billing-step
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[organizationInfo]="{
|
||||
name: orgInfoFormGroup.get('name').value,
|
||||
email: orgInfoFormGroup.get('email').value,
|
||||
type: trialOrganizationType,
|
||||
}"
|
||||
[subscriptionProduct]="SubscriptionProduct.PasswordManager"
|
||||
(steppedBack)="previousStep()"
|
||||
(organizationCreated)="createdOrganization($event)"
|
||||
>
|
||||
</app-trial-billing-step>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
||||
<app-trial-confirmation-details
|
||||
[email]="email"
|
||||
[orgLabel]="orgLabel"
|
||||
></app-trial-confirmation-details>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="navigateToOrgVault()"
|
||||
>
|
||||
{{ "getStarted" | i18n | titlecase }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="navigateToOrgInvite()"
|
||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
||||
>
|
||||
{{ "inviteUsers" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</app-vertical-step>
|
||||
</app-vertical-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,336 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
||||
import { RouterService } from "../../core";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
describe("TrialInitiationComponent", () => {
|
||||
let component: TrialInitiationComponent;
|
||||
let fixture: ComponentFixture<TrialInitiationComponent>;
|
||||
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
|
||||
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
|
||||
const formBuilder: FormBuilder = new FormBuilder();
|
||||
let routerSpy: jest.SpyInstance;
|
||||
|
||||
let stateServiceMock: MockProxy<StateService>;
|
||||
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let policyServiceMock: MockProxy<PolicyService>;
|
||||
let routerServiceMock: MockProxy<RouterService>;
|
||||
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
||||
let organizationBillingServiceMock: MockProxy<OrganizationBillingService>;
|
||||
let configServiceMock: MockProxy<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
// only define services directly that we want to mock return values in this component
|
||||
stateServiceMock = mock<StateService>();
|
||||
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
||||
policyServiceMock = mock<PolicyService>();
|
||||
routerServiceMock = mock<RouterService>();
|
||||
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
||||
organizationBillingServiceMock = mock<OrganizationBillingService>();
|
||||
configServiceMock = mock<ConfigService>();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: "trial", component: TrialInitiationComponent },
|
||||
{
|
||||
path: `organizations/${testOrgId}/vault`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
{
|
||||
path: `organizations/${testOrgId}/members`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
declarations: [TrialInitiationComponent, I18nPipe],
|
||||
providers: [
|
||||
UntypedFormBuilder,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParams: mockQueryParams.asObservable(),
|
||||
},
|
||||
},
|
||||
{ provide: StateService, useValue: stateServiceMock },
|
||||
{ provide: PolicyService, useValue: policyServiceMock },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: TitleCasePipe, useValue: mock<TitleCasePipe>() },
|
||||
{
|
||||
provide: VerticalStepperComponent,
|
||||
useClass: VerticalStepperStubComponent,
|
||||
},
|
||||
{
|
||||
provide: RouterService,
|
||||
useValue: routerServiceMock,
|
||||
},
|
||||
{
|
||||
provide: AcceptOrganizationInviteService,
|
||||
useValue: acceptOrgInviteServiceMock,
|
||||
},
|
||||
{
|
||||
provide: OrganizationBillingService,
|
||||
useValue: organizationBillingServiceMock,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: configServiceMock,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// These tests demonstrate mocking service calls
|
||||
describe("onInit() enforcedPolicyOptions", () => {
|
||||
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
||||
// Need to recreate component with new service mock
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
||||
});
|
||||
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
||||
// Set up service method mocks
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
||||
organizationId: testOrgId,
|
||||
token: "token",
|
||||
email: "testEmail",
|
||||
organizationUserId: "123",
|
||||
} as OrganizationInvite);
|
||||
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
||||
Promise.resolve([
|
||||
{
|
||||
id: "345",
|
||||
organizationId: testOrgId,
|
||||
type: 1,
|
||||
data: {
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
] as Policy[]),
|
||||
);
|
||||
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
||||
of({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
} as MasterPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
// Need to recreate component with new service mocks
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
expect(component.enforcedPolicyOptions).toMatchObject({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These tests demonstrate route params
|
||||
describe("Route params", () => {
|
||||
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
|
||||
mockQueryParams.next({ org: "enterprise" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("enterprise");
|
||||
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
|
||||
}));
|
||||
it("should not set org variable if no org param is provided", fakeAsync(() => {
|
||||
mockQueryParams.next({});
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
it("should not set the org if org param is invalid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ org: "hahahaha" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
it("should set the layout variable if layout param is valid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ layout: "teams1" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.layout).toBe("teams1");
|
||||
expect(component.accountCreateOnly).toBe(false);
|
||||
}));
|
||||
it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ layout: "asdfasdf" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component.ngOnInit();
|
||||
expect(component.layout).toBe("default");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate the use of a stub component
|
||||
describe("createAccount()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set email and call verticalStepper.next()", fakeAsync(() => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.createdAccount("test@email.com");
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.email).toBe("test@email.com");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("billingSuccess()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set orgId and call verticalStepper.next()", () => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.billingSuccess({ orgId: testOrgId });
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.orgId).toBe(testOrgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stepSelectionChange()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("on step 2 should show organization copy text", () => {
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 1,
|
||||
previouslySelectedIndex: 0,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Enter your");
|
||||
expect(component.orgInfoSubLabel).toContain(" organization information");
|
||||
});
|
||||
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
|
||||
component.orgInfoFormGroup = formBuilder.group({
|
||||
name: ["Hooli"],
|
||||
email: [""],
|
||||
});
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 2,
|
||||
previouslySelectedIndex: 1,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Hooli");
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
|
||||
component.previousStep();
|
||||
expect(verticalStepperPrevious).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate router navigation
|
||||
describe("navigation methods", () => {
|
||||
beforeEach(() => {
|
||||
component.orgId = testOrgId;
|
||||
const router = TestBed.inject(Router);
|
||||
fixture.detectChanges();
|
||||
routerSpy = jest.spyOn(router, "navigate");
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgVault();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
|
||||
}));
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgInvite();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
|
||||
export class BlankComponent {} // For router tests
|
||||
@@ -1,353 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import {
|
||||
OrganizationInformation,
|
||||
PlanInformation,
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
SubscriptionProduct,
|
||||
TrialOrganizationType,
|
||||
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
|
||||
import { RouterService } from "./../../core/router.service";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
export enum ValidOrgParams {
|
||||
families = "families",
|
||||
enterprise = "enterprise",
|
||||
teams = "teams",
|
||||
teamsStarter = "teamsStarter",
|
||||
individual = "individual",
|
||||
premium = "premium",
|
||||
free = "free",
|
||||
}
|
||||
|
||||
enum ValidLayoutParams {
|
||||
default = "default",
|
||||
teams = "teams",
|
||||
teams1 = "teams1",
|
||||
teams2 = "teams2",
|
||||
teams3 = "teams3",
|
||||
enterprise = "enterprise",
|
||||
enterprise1 = "enterprise1",
|
||||
enterprise2 = "enterprise2",
|
||||
cnetcmpgnent = "cnetcmpgnent",
|
||||
cnetcmpgnind = "cnetcmpgnind",
|
||||
cnetcmpgnteams = "cnetcmpgnteams",
|
||||
abmenterprise = "abmenterprise",
|
||||
abmteams = "abmteams",
|
||||
secretsManager = "secretsManager",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-trial",
|
||||
templateUrl: "trial-initiation.component.html",
|
||||
})
|
||||
export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
email = "";
|
||||
fromOrgInvite = false;
|
||||
org = "";
|
||||
orgInfoSubLabel = "";
|
||||
orgId = "";
|
||||
orgLabel = "";
|
||||
billingSubLabel = "";
|
||||
layout = "default";
|
||||
plan: PlanType;
|
||||
productTier: ProductTierType;
|
||||
accountCreateOnly = true;
|
||||
useTrialStepper = false;
|
||||
loading = false;
|
||||
policies: Policy[];
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
trialFlowOrgs: string[] = [
|
||||
ValidOrgParams.teams,
|
||||
ValidOrgParams.teamsStarter,
|
||||
ValidOrgParams.enterprise,
|
||||
ValidOrgParams.families,
|
||||
];
|
||||
routeFlowOrgs: string[] = [
|
||||
ValidOrgParams.free,
|
||||
ValidOrgParams.premium,
|
||||
ValidOrgParams.individual,
|
||||
];
|
||||
layouts = ValidLayoutParams;
|
||||
referenceData: ReferenceEventRequest;
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
||||
email: [""],
|
||||
});
|
||||
|
||||
private set referenceDataId(referenceId: string) {
|
||||
if (referenceId != null) {
|
||||
this.referenceData.id = referenceId;
|
||||
} else {
|
||||
this.referenceData.id = ("; " + document.cookie)
|
||||
.split("; reference=")
|
||||
.pop()
|
||||
.split(";")
|
||||
.shift();
|
||||
}
|
||||
|
||||
if (this.referenceData.id === "") {
|
||||
this.referenceData.id = null;
|
||||
} else {
|
||||
// Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session.
|
||||
const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/;
|
||||
const match = document.cookie.match(regex);
|
||||
if (match) {
|
||||
this.referenceData.session = match[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
protected enableTrialPayment$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.TrialPaymentOptional,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private logService: LogService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private i18nService: I18nService,
|
||||
private routerService: RouterService,
|
||||
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
|
||||
this.referenceData = new ReferenceEventRequest();
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
this.fromOrgInvite = qParams.fromOrgInvite === "true";
|
||||
}
|
||||
|
||||
this.referenceDataId = qParams.reference;
|
||||
|
||||
if (Object.values(ValidLayoutParams).includes(qParams.layout)) {
|
||||
this.layout = qParams.layout;
|
||||
this.accountCreateOnly = false;
|
||||
}
|
||||
|
||||
if (this.trialFlowOrgs.includes(qParams.org)) {
|
||||
this.org = qParams.org;
|
||||
this.orgLabel = this.titleCasePipe.transform(this.orgDisplayName);
|
||||
this.useTrialStepper = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
|
||||
if (this.org === ValidOrgParams.families) {
|
||||
this.plan = PlanType.FamiliesAnnually;
|
||||
this.productTier = ProductTierType.Families;
|
||||
} else if (this.org === ValidOrgParams.teamsStarter) {
|
||||
this.plan = PlanType.TeamsStarter;
|
||||
this.productTier = ProductTierType.TeamsStarter;
|
||||
} else if (this.org === ValidOrgParams.teams) {
|
||||
this.plan = PlanType.TeamsAnnually;
|
||||
this.productTier = ProductTierType.Teams;
|
||||
} else if (this.org === ValidOrgParams.enterprise) {
|
||||
this.plan = PlanType.EnterpriseAnnually;
|
||||
this.productTier = ProductTierType.Enterprise;
|
||||
}
|
||||
} else if (this.routeFlowOrgs.includes(qParams.org)) {
|
||||
this.referenceData.flow = qParams.org;
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
// After logging in redirect them to setup the families sponsorship
|
||||
this.setupFamilySponsorship(qParams.sponsorshipToken);
|
||||
|
||||
this.referenceData.initiationPath = this.accountCreateOnly
|
||||
? "Registration form"
|
||||
: "Password Manager trial from marketing website";
|
||||
});
|
||||
|
||||
// If there's a deep linked org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
|
||||
this.orgInfoFormGroup.controls.name.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
stepSelectionChange(event: StepperSelectionEvent) {
|
||||
// Set org info sub label
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel =
|
||||
"Enter your " +
|
||||
this.titleCasePipe.transform(this.orgDisplayName) +
|
||||
" organization information";
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
}
|
||||
|
||||
//set billing sub label
|
||||
if (event.selectedIndex === 2) {
|
||||
this.billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
||||
}
|
||||
}
|
||||
|
||||
async createOrganizationOnTrial() {
|
||||
this.loading = true;
|
||||
const organization: OrganizationInformation = {
|
||||
name: this.orgInfoFormGroup.get("name").value,
|
||||
billingEmail: this.orgInfoFormGroup.get("email").value,
|
||||
initiationPath: "Password Manager trial from marketing website",
|
||||
};
|
||||
|
||||
const plan: PlanInformation = {
|
||||
type: this.plan,
|
||||
passwordManagerSeats: 1,
|
||||
};
|
||||
|
||||
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
|
||||
organization,
|
||||
plan,
|
||||
});
|
||||
|
||||
this.orgId = response?.id;
|
||||
this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`;
|
||||
this.loading = false;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
createdAccount(email: string) {
|
||||
this.email = email;
|
||||
this.orgInfoFormGroup.get("email")?.setValue(email);
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
billingSuccess(event: any) {
|
||||
this.orgId = event?.orgId;
|
||||
this.billingSubLabel = event?.subLabelText;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
createdOrganization(event: OrganizationCreatedEvent) {
|
||||
this.orgId = event.organizationId;
|
||||
this.billingSubLabel = event.planDescription;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
navigateToOrgVault() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["organizations", this.orgId, "vault"]);
|
||||
}
|
||||
|
||||
navigateToOrgInvite() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["organizations", this.orgId, "members"]);
|
||||
}
|
||||
|
||||
previousStep() {
|
||||
this.verticalStepper.previous();
|
||||
}
|
||||
|
||||
get orgDisplayName() {
|
||||
if (this.org === "teamsStarter") {
|
||||
return "Teams Starter";
|
||||
}
|
||||
|
||||
return this.org;
|
||||
}
|
||||
|
||||
get freeTrialText() {
|
||||
const translationKey =
|
||||
this.layout === this.layouts.secretsManager
|
||||
? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
|
||||
: "startYour7DayFreeTrialOfBitwardenFor";
|
||||
|
||||
return this.i18nService.t(translationKey, this.org);
|
||||
}
|
||||
|
||||
get trialOrganizationType(): TrialOrganizationType {
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
return null;
|
||||
default:
|
||||
return this.productTier;
|
||||
}
|
||||
}
|
||||
|
||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
||||
if (sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { plan: sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
if (invite == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular";
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||
import { RegisterFormModule } from "../../auth/register-form/register-form.module";
|
||||
import { TaxInfoComponent } from "../../billing";
|
||||
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||
@@ -39,7 +38,6 @@ import { TeamsContentComponent } from "./content/teams-content.component";
|
||||
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
||||
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
||||
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||
|
||||
@NgModule({
|
||||
@@ -48,7 +46,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
CdkStepperModule,
|
||||
VerticalStepperModule,
|
||||
FormFieldModule,
|
||||
RegisterFormModule,
|
||||
OrganizationCreateModule,
|
||||
EnvironmentSelectorModule,
|
||||
TaxInfoComponent,
|
||||
@@ -56,7 +53,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
InputPasswordComponent,
|
||||
],
|
||||
declarations: [
|
||||
TrialInitiationComponent,
|
||||
CompleteTrialInitiationComponent,
|
||||
EnterpriseContentComponent,
|
||||
TeamsContentComponent,
|
||||
@@ -87,7 +83,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
SecretsManagerTrialFreeStepperComponent,
|
||||
SecretsManagerTrialPaidStepperComponent,
|
||||
],
|
||||
exports: [TrialInitiationComponent, CompleteTrialInitiationComponent],
|
||||
exports: [CompleteTrialInitiationComponent],
|
||||
providers: [TitleCasePipe],
|
||||
})
|
||||
export class TrialInitiationModule {}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||
import { RegisterFormModule } from "../auth/register-form/register-form.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { AccountComponent } from "../auth/settings/account/account.component";
|
||||
@@ -90,7 +89,6 @@ import { SharedModule } from "./shared.module";
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RegisterFormModule,
|
||||
ProductSwitcherModule,
|
||||
UserVerificationModule,
|
||||
ChangeKdfModule,
|
||||
|
||||
@@ -10333,5 +10333,18 @@
|
||||
"example": "Acme c"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seatsRemaining": {
|
||||
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
|
||||
"placeholders": {
|
||||
"remaining": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"total": {
|
||||
"content": "$2",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
|
||||
/**
|
||||
* Request type for creating tasks.
|
||||
* @property cipherId - Optional. The ID of the cipher to create the task for.
|
||||
* @property type - The type of task to create. Currently defined as "updateAtRiskCredential".
|
||||
*/
|
||||
export type CreateTasksRequest = Readonly<{
|
||||
cipherId?: CipherId;
|
||||
type: SecurityTaskType.UpdateAtRiskCredential;
|
||||
}>;
|
||||
|
||||
export abstract class AdminTaskService {
|
||||
/**
|
||||
* Retrieves all tasks for a given organization.
|
||||
* @param organizationId - The ID of the organization to retrieve tasks for.
|
||||
* @param status - Optional. The status of the tasks to retrieve.
|
||||
*/
|
||||
abstract getAllTasks(
|
||||
organizationId: OrganizationId,
|
||||
status?: SecurityTaskStatus | undefined,
|
||||
): Promise<SecurityTask[]>;
|
||||
|
||||
/**
|
||||
* Creates multiple tasks for a given organization and sends out notifications to applicable users.
|
||||
* @param organizationId - The ID of the organization to create tasks for.
|
||||
* @param tasks - The tasks to create.
|
||||
*/
|
||||
abstract bulkCreateTasks(
|
||||
organizationId: OrganizationId,
|
||||
tasks: CreateTasksRequest[],
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
|
||||
import { CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "./default-admin-task.service";
|
||||
|
||||
describe("DefaultAdminTaskService", () => {
|
||||
let defaultAdminTaskService: DefaultAdminTaskService;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
defaultAdminTaskService = new DefaultAdminTaskService(apiService);
|
||||
});
|
||||
|
||||
describe("getAllTasks", () => {
|
||||
it("should call the api service with the correct parameters with status", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const status = SecurityTaskStatus.Pending;
|
||||
const expectedUrl = `/tasks/organization?organizationId=${organizationId}&status=0`;
|
||||
|
||||
await defaultAdminTaskService.getAllTasks(organizationId, status);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true);
|
||||
});
|
||||
|
||||
it("should call the api service with the correct parameters without status", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const expectedUrl = `/tasks/organization?organizationId=${organizationId}`;
|
||||
|
||||
await defaultAdminTaskService.getAllTasks(organizationId);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bulkCreateTasks", () => {
|
||||
it("should call the api service with the correct parameters", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const tasks: CreateTasksRequest[] = [
|
||||
{
|
||||
cipherId: "cipherId-1" as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
},
|
||||
{
|
||||
cipherId: "cipherId-2" as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
},
|
||||
];
|
||||
|
||||
await defaultAdminTaskService.bulkCreateTasks(organizationId, tasks);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/tasks/${organizationId}/bulk-create`,
|
||||
tasks,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
SecurityTask,
|
||||
SecurityTaskData,
|
||||
SecurityTaskResponse,
|
||||
SecurityTaskStatus,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { AdminTaskService, CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class DefaultAdminTaskService implements AdminTaskService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getAllTasks(
|
||||
organizationId: OrganizationId,
|
||||
status?: SecurityTaskStatus | undefined,
|
||||
): Promise<SecurityTask[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
queryParams.append("organizationId", organizationId);
|
||||
if (status !== undefined) {
|
||||
queryParams.append("status", status.toString());
|
||||
}
|
||||
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/tasks/organization?${queryParams.toString()}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
const response = new ListResponse(r, SecurityTaskResponse);
|
||||
|
||||
return response.data.map((d) => new SecurityTask(new SecurityTaskData(d)));
|
||||
}
|
||||
|
||||
async bulkCreateTasks(
|
||||
organizationId: OrganizationId,
|
||||
tasks: CreateTasksRequest[],
|
||||
): Promise<void> {
|
||||
await this.apiService.send("POST", `/tasks/${organizationId}/bulk-create`, tasks, true, true);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import rxjs from "eslint-plugin-rxjs";
|
||||
import angularRxjs from "eslint-plugin-rxjs-angular";
|
||||
import storybook from "eslint-plugin-storybook";
|
||||
|
||||
import platformPlugins from "./libs/eslint/platform/index.mjs";
|
||||
|
||||
export default tseslint.config(
|
||||
...storybook.configs["flat/recommended"],
|
||||
{
|
||||
@@ -28,6 +30,7 @@ export default tseslint.config(
|
||||
plugins: {
|
||||
rxjs: rxjs,
|
||||
"rxjs-angular": angularRxjs,
|
||||
"@bitwarden/platform": platformPlugins,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
@@ -66,7 +69,7 @@ export default tseslint.config(
|
||||
"@angular-eslint/no-outputs-metadata-property": 0,
|
||||
"@angular-eslint/use-lifecycle-interface": "error",
|
||||
"@angular-eslint/use-pipe-transform-interface": 0,
|
||||
|
||||
"@bitwarden/platform/required-using": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
"<rootDir>/libs/billing/jest.config.js",
|
||||
"<rootDir>/libs/common/jest.config.js",
|
||||
"<rootDir>/libs/components/jest.config.js",
|
||||
"<rootDir>/libs/eslint/jest.config.js",
|
||||
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
|
||||
"<rootDir>/libs/tools/generator/core/jest.config.js",
|
||||
"<rootDir>/libs/tools/generator/components/jest.config.js",
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<div class="tw-mb-6" *ngIf="sentInitialCode">
|
||||
{{ "enterVerificationCodeSentToEmail" | i18n }}
|
||||
|
||||
<p class="mb-0">
|
||||
<p class="tw-mb-0">
|
||||
<button bitLink type="button" linkType="primary" (click)="requestOTP()">
|
||||
{{ "resendCode" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Observable } from "rxjs";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
|
||||
export abstract class SdkService {
|
||||
/**
|
||||
@@ -27,5 +28,5 @@ export abstract class SdkService {
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
abstract userClient$(userId: UserId): Observable<BitwardenClient | undefined>;
|
||||
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined>;
|
||||
}
|
||||
|
||||
93
libs/common/src/platform/misc/reference-counting/rc.spec.ts
Normal file
93
libs/common/src/platform/misc/reference-counting/rc.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Temporary workaround for Symbol.dispose
|
||||
// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released*
|
||||
const disposeSymbol: unique symbol = Symbol("Symbol.dispose");
|
||||
const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose");
|
||||
(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"];
|
||||
(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"];
|
||||
|
||||
// Import needs to be after the workaround
|
||||
import { Rc } from "./rc";
|
||||
|
||||
export class FreeableTestValue {
|
||||
isFreed = false;
|
||||
|
||||
free() {
|
||||
this.isFreed = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe("Rc", () => {
|
||||
let value: FreeableTestValue;
|
||||
let rc: Rc<FreeableTestValue>;
|
||||
|
||||
beforeEach(() => {
|
||||
value = new FreeableTestValue();
|
||||
rc = new Rc(value);
|
||||
});
|
||||
|
||||
it("should increase refCount when taken", () => {
|
||||
rc.take();
|
||||
|
||||
expect(rc["refCount"]).toBe(1);
|
||||
});
|
||||
|
||||
it("should return value on take", () => {
|
||||
// eslint-disable-next-line @bitwarden/platform/required-using
|
||||
const reference = rc.take();
|
||||
|
||||
expect(reference.value).toBe(value);
|
||||
});
|
||||
|
||||
it("should decrease refCount when disposing reference", () => {
|
||||
// eslint-disable-next-line @bitwarden/platform/required-using
|
||||
const reference = rc.take();
|
||||
|
||||
reference[Symbol.dispose]();
|
||||
|
||||
expect(rc["refCount"]).toBe(0);
|
||||
});
|
||||
|
||||
it("should automatically decrease refCount when reference goes out of scope", () => {
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
using reference = rc.take();
|
||||
}
|
||||
|
||||
expect(rc["refCount"]).toBe(0);
|
||||
});
|
||||
|
||||
it("should not free value when refCount reaches 0 if not marked for disposal", () => {
|
||||
// eslint-disable-next-line @bitwarden/platform/required-using
|
||||
const reference = rc.take();
|
||||
|
||||
reference[Symbol.dispose]();
|
||||
|
||||
expect(value.isFreed).toBe(false);
|
||||
});
|
||||
|
||||
it("should free value when refCount reaches 0 and rc is marked for disposal", () => {
|
||||
// eslint-disable-next-line @bitwarden/platform/required-using
|
||||
const reference = rc.take();
|
||||
rc.markForDisposal();
|
||||
|
||||
reference[Symbol.dispose]();
|
||||
|
||||
expect(value.isFreed).toBe(true);
|
||||
});
|
||||
|
||||
it("should free value when marked for disposal if refCount is 0", () => {
|
||||
// eslint-disable-next-line @bitwarden/platform/required-using
|
||||
const reference = rc.take();
|
||||
reference[Symbol.dispose]();
|
||||
|
||||
rc.markForDisposal();
|
||||
|
||||
expect(value.isFreed).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw error when trying to take a disposed reference", () => {
|
||||
rc.markForDisposal();
|
||||
|
||||
expect(() => rc.take()).toThrow();
|
||||
});
|
||||
});
|
||||
76
libs/common/src/platform/misc/reference-counting/rc.ts
Normal file
76
libs/common/src/platform/misc/reference-counting/rc.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { UsingRequired } from "../using-required";
|
||||
|
||||
export type Freeable = { free: () => void };
|
||||
|
||||
/**
|
||||
* Reference counted disposable value.
|
||||
* This class is used to manage the lifetime of a value that needs to be
|
||||
* freed of at a specific time but might still be in-use when that happens.
|
||||
*/
|
||||
export class Rc<T extends Freeable> {
|
||||
private markedForDisposal = false;
|
||||
private refCount = 0;
|
||||
private value: T;
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function when you want to use the underlying object.
|
||||
* This will guarantee that you have a reference to the object
|
||||
* and that it won't be freed until your reference goes out of scope.
|
||||
*
|
||||
* This function must be used with the `using` keyword.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function someFunction(rc: Rc<SomeValue>) {
|
||||
* using reference = rc.take();
|
||||
* reference.value.doSomething();
|
||||
* // reference is automatically disposed here
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns The value.
|
||||
*/
|
||||
take(): Ref<T> {
|
||||
if (this.markedForDisposal) {
|
||||
throw new Error("Cannot take a reference to a value marked for disposal");
|
||||
}
|
||||
|
||||
this.refCount++;
|
||||
return new Ref(() => this.release(), this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this Rc for disposal. When the refCount reaches 0, the value
|
||||
* will be freed.
|
||||
*/
|
||||
markForDisposal() {
|
||||
this.markedForDisposal = true;
|
||||
this.freeIfPossible();
|
||||
}
|
||||
|
||||
private release() {
|
||||
this.refCount--;
|
||||
this.freeIfPossible();
|
||||
}
|
||||
|
||||
private freeIfPossible() {
|
||||
if (this.refCount === 0 && this.markedForDisposal) {
|
||||
this.value.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Ref<T extends Freeable> implements UsingRequired {
|
||||
constructor(
|
||||
private readonly release: () => void,
|
||||
readonly value: T,
|
||||
) {}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.release();
|
||||
}
|
||||
}
|
||||
11
libs/common/src/platform/misc/using-required.ts
Normal file
11
libs/common/src/platform/misc/using-required.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type Disposable = { [Symbol.dispose]: () => void };
|
||||
|
||||
/**
|
||||
* Types implementing this type must be used together with the `using` keyword
|
||||
*
|
||||
* @example using ref = rc.take();
|
||||
*/
|
||||
// We want to use `interface` here because it creates a separate type.
|
||||
// Type aliasing would not expose `UsingRequired` to the linter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UsingRequired extends Disposable {}
|
||||
@@ -10,6 +10,7 @@ import { UserKey } from "../../../types/key";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
@@ -75,15 +76,14 @@ describe("DefaultSdkService", () => {
|
||||
});
|
||||
|
||||
it("creates an SDK client when called the first time", async () => {
|
||||
const result = await firstValueFrom(service.userClient$(userId));
|
||||
await firstValueFrom(service.userClient$(userId));
|
||||
|
||||
expect(result).toBe(mockClient);
|
||||
expect(sdkClientFactory.createSdkClient).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not create an SDK client when called the second time with same userId", async () => {
|
||||
const subject_1 = new BehaviorSubject(undefined);
|
||||
const subject_2 = new BehaviorSubject(undefined);
|
||||
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
||||
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
||||
|
||||
// Use subjects to ensure the subscription is kept alive
|
||||
service.userClient$(userId).subscribe(subject_1);
|
||||
@@ -92,14 +92,14 @@ describe("DefaultSdkService", () => {
|
||||
// Wait for the next tick to ensure all async operations are done
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(subject_1.value).toBe(mockClient);
|
||||
expect(subject_2.value).toBe(mockClient);
|
||||
expect(subject_1.value.take().value).toBe(mockClient);
|
||||
expect(subject_2.value.take().value).toBe(mockClient);
|
||||
expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("destroys the SDK client when all subscriptions are closed", async () => {
|
||||
const subject_1 = new BehaviorSubject(undefined);
|
||||
const subject_2 = new BehaviorSubject(undefined);
|
||||
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
||||
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
||||
const subscription_1 = service.userClient$(userId).subscribe(subject_1);
|
||||
const subscription_2 = service.userClient$(userId).subscribe(subject_2);
|
||||
await new Promise(process.nextTick);
|
||||
@@ -107,6 +107,7 @@ describe("DefaultSdkService", () => {
|
||||
subscription_1.unsubscribe();
|
||||
subscription_2.unsubscribe();
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
expect(mockClient.free).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -114,7 +115,7 @@ describe("DefaultSdkService", () => {
|
||||
const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey);
|
||||
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
|
||||
|
||||
const subject = new BehaviorSubject(undefined);
|
||||
const subject = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
||||
service.userClient$(userId).subscribe(subject);
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
|
||||
@@ -30,10 +30,11 @@ import { PlatformUtilsService } from "../../abstractions/platform-utils.service"
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
import { SdkService } from "../../abstractions/sdk/sdk.service";
|
||||
import { compareValues } from "../../misc/compare-values";
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
|
||||
export class DefaultSdkService implements SdkService {
|
||||
private sdkClientCache = new Map<UserId, Observable<BitwardenClient>>();
|
||||
private sdkClientCache = new Map<UserId, Observable<Rc<BitwardenClient>>>();
|
||||
|
||||
client$ = this.environmentService.environment$.pipe(
|
||||
concatMap(async (env) => {
|
||||
@@ -58,7 +59,7 @@ export class DefaultSdkService implements SdkService {
|
||||
private userAgent: string = null,
|
||||
) {}
|
||||
|
||||
userClient$(userId: UserId): Observable<BitwardenClient | undefined> {
|
||||
userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined> {
|
||||
// TODO: Figure out what happens when the user logs out
|
||||
if (this.sdkClientCache.has(userId)) {
|
||||
return this.sdkClientCache.get(userId);
|
||||
@@ -88,32 +89,31 @@ export class DefaultSdkService implements SdkService {
|
||||
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
|
||||
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
|
||||
// Create our own observable to be able to implement clean-up logic
|
||||
return new Observable<BitwardenClient>((subscriber) => {
|
||||
let client: BitwardenClient;
|
||||
|
||||
return new Observable<Rc<BitwardenClient>>((subscriber) => {
|
||||
const createAndInitializeClient = async () => {
|
||||
if (privateKey == null || userKey == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const settings = this.toSettings(env);
|
||||
client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
|
||||
const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
|
||||
|
||||
await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys);
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
let client: Rc<BitwardenClient>;
|
||||
createAndInitializeClient()
|
||||
.then((c) => {
|
||||
client = c;
|
||||
subscriber.next(c);
|
||||
client = c === undefined ? undefined : new Rc(c);
|
||||
subscriber.next(client);
|
||||
})
|
||||
.catch((e) => {
|
||||
subscriber.error(e);
|
||||
});
|
||||
|
||||
return () => client?.free();
|
||||
return () => client?.markForDisposal();
|
||||
});
|
||||
}),
|
||||
tap({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf, NgClass } from "@angular/common";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
|
||||
@@ -18,9 +18,11 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
|
||||
@Component({
|
||||
selector: "bit-avatar",
|
||||
template: `<img *ngIf="src" [src]="src" title="{{ title || text }}" [ngClass]="classList" />`,
|
||||
template: `@if (src) {
|
||||
<img [src]="src" title="{{ title || text }}" [ngClass]="classList" />
|
||||
}`,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgClass],
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class AvatarComponent implements OnChanges {
|
||||
@Input() border = false;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<div class="tw-inline-flex tw-flex-wrap tw-gap-2">
|
||||
<ng-container *ngFor="let item of filteredItems; let last = last">
|
||||
@for (item of filteredItems; track item; let last = $last) {
|
||||
<span bitBadge [variant]="variant" [truncate]="truncate">
|
||||
{{ item }}
|
||||
</span>
|
||||
<span class="tw-sr-only" *ngIf="!last || isFiltered">, </span>
|
||||
</ng-container>
|
||||
<span *ngIf="isFiltered" bitBadge [variant]="variant">
|
||||
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
|
||||
</span>
|
||||
@if (!last || isFiltered) {
|
||||
<span class="tw-sr-only">, </span>
|
||||
}
|
||||
}
|
||||
@if (isFiltered) {
|
||||
<span bitBadge [variant]="variant">
|
||||
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -11,7 +11,7 @@ import { BadgeModule, BadgeVariant } from "../badge";
|
||||
selector: "bit-badge-list",
|
||||
templateUrl: "badge-list.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, BadgeModule, I18nPipe],
|
||||
imports: [BadgeModule, I18nPipe],
|
||||
})
|
||||
export class BadgeListComponent implements OnChanges {
|
||||
private _maxItems: number;
|
||||
|
||||
@@ -4,21 +4,24 @@
|
||||
[attr.role]="useAlertRole ? 'status' : null"
|
||||
[attr.aria-live]="useAlertRole ? 'polite' : null"
|
||||
>
|
||||
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" *ngIf="icon" aria-hidden="true"></i>
|
||||
@if (icon) {
|
||||
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" aria-hidden="true"></i>
|
||||
}
|
||||
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
|
||||
<span class="tw-grow tw-text-base [&>button[bitlink]:focus-visible:before]:!tw-ring-text-main">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
|
||||
<button
|
||||
*ngIf="showClose"
|
||||
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
size="default"
|
||||
(click)="onClose.emit()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
></button>
|
||||
@if (showClose) {
|
||||
<button
|
||||
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
size="default"
|
||||
(click)="onClose.emit()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
></button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<ng-template>
|
||||
<i *ngIf="icon" class="bwi {{ icon }} !tw-mr-2" aria-hidden="true"></i><ng-content></ng-content>
|
||||
@if (icon) {
|
||||
<i class="bwi {{ icon }} !tw-mr-2" aria-hidden="true"></i>
|
||||
}
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf } from "@angular/common";
|
||||
|
||||
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { QueryParamsHandling } from "@angular/router";
|
||||
|
||||
@@ -8,7 +8,6 @@ import { QueryParamsHandling } from "@angular/router";
|
||||
selector: "bit-breadcrumb",
|
||||
templateUrl: "./breadcrumb.component.html",
|
||||
standalone: true,
|
||||
imports: [NgIf],
|
||||
})
|
||||
export class BreadcrumbComponent {
|
||||
@Input()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngFor="let breadcrumb of beforeOverflow; let last = last">
|
||||
<ng-container *ngIf="breadcrumb.route">
|
||||
@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) {
|
||||
@if (breadcrumb.route) {
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
@@ -10,8 +10,8 @@
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!breadcrumb.route">
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
@@ -21,13 +21,16 @@
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</button>
|
||||
</ng-container>
|
||||
<i *ngIf="!last" class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="hasOverflow">
|
||||
<i *ngIf="beforeOverflow.length > 0" class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
}
|
||||
@if (!last) {
|
||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
}
|
||||
}
|
||||
|
||||
@if (hasOverflow) {
|
||||
@if (beforeOverflow.length > 0) {
|
||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-h"
|
||||
@@ -35,10 +38,9 @@
|
||||
size="small"
|
||||
aria-haspopup
|
||||
></button>
|
||||
|
||||
<bit-menu #overflowMenu>
|
||||
<ng-container *ngFor="let breadcrumb of overflow">
|
||||
<ng-container *ngIf="breadcrumb.route">
|
||||
@for (breadcrumb of overflow; track breadcrumb) {
|
||||
@if (breadcrumb.route) {
|
||||
<a
|
||||
bitMenuItem
|
||||
linkType="primary"
|
||||
@@ -48,18 +50,17 @@
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!breadcrumb.route">
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</bit-menu>
|
||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
|
||||
<ng-container *ngFor="let breadcrumb of afterOverflow; let last = last">
|
||||
<ng-container *ngIf="breadcrumb.route">
|
||||
@for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) {
|
||||
@if (breadcrumb.route) {
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
@@ -70,8 +71,8 @@
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!breadcrumb.route">
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
@@ -81,7 +82,9 @@
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</button>
|
||||
</ng-container>
|
||||
<i *ngIf="!last" class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!last) {
|
||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +86,15 @@ export const DisabledWithAttribute: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ng-container *ngIf="disabled">
|
||||
@if (disabled) {
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!disabled">
|
||||
} @else {
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
}
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
[ngClass]="calloutClass"
|
||||
[attr.aria-labelledby]="titleId"
|
||||
>
|
||||
<header id="{{ titleId }}" class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold" *ngIf="title">
|
||||
<i class="bwi" [ngClass]="[icon, headerClass]" *ngIf="icon" aria-hidden="true"></i>
|
||||
{{ title }}
|
||||
</header>
|
||||
@if (title) {
|
||||
<header id="{{ titleId }}" class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold">
|
||||
@if (icon) {
|
||||
<i class="bwi" [ngClass]="[icon, headerClass]" aria-hidden="true"></i>
|
||||
}
|
||||
{{ title }}
|
||||
</header>
|
||||
}
|
||||
<div bitTypography="body2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "bit-card",
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
|
||||
@@ -30,78 +30,80 @@
|
||||
<i class="bwi !tw-text-[inherit]" [ngClass]="icon"></i>
|
||||
<span class="tw-truncate">{{ label }}</span>
|
||||
</span>
|
||||
<i
|
||||
*ngIf="!selectedOption"
|
||||
class="bwi tw-mt-0.5"
|
||||
[ngClass]="menuTrigger.isOpen ? 'bwi-angle-up' : 'bwi-angle-down'"
|
||||
></i>
|
||||
@if (!selectedOption) {
|
||||
<i
|
||||
class="bwi tw-mt-0.5"
|
||||
[ngClass]="menuTrigger.isOpen ? 'bwi-angle-up' : 'bwi-angle-down'"
|
||||
></i>
|
||||
}
|
||||
</button>
|
||||
|
||||
<!-- Close button -->
|
||||
<button
|
||||
*ngIf="selectedOption"
|
||||
type="button"
|
||||
[attr.aria-label]="'removeItem' | i18n: label"
|
||||
[disabled]="disabled"
|
||||
class="tw-bg-transparent hover:tw-bg-transparent tw-outline-none tw-rounded-full tw-py-0.5 tw-px-1 tw-mr-1 tw-text-[color:inherit] tw-text-[length:inherit] tw-border-solid tw-border tw-border-transparent hover:tw-border-text-contrast hover:disabled:tw-border-transparent tw-flex tw-items-center tw-justify-center focus-visible:tw-ring-2 tw-ring-text-contrast focus-visible:hover:tw-border-transparent"
|
||||
[ngClass]="{
|
||||
'tw-cursor-not-allowed': disabled,
|
||||
}"
|
||||
(click)="clear()"
|
||||
>
|
||||
<i class="bwi bwi-close tw-text-xs"></i>
|
||||
</button>
|
||||
@if (selectedOption) {
|
||||
<button
|
||||
type="button"
|
||||
[attr.aria-label]="'removeItem' | i18n: label"
|
||||
[disabled]="disabled"
|
||||
class="tw-bg-transparent hover:tw-bg-transparent tw-outline-none tw-rounded-full tw-py-0.5 tw-px-1 tw-mr-1 tw-text-[color:inherit] tw-text-[length:inherit] tw-border-solid tw-border tw-border-transparent hover:tw-border-text-contrast hover:disabled:tw-border-transparent tw-flex tw-items-center tw-justify-center focus-visible:tw-ring-2 tw-ring-text-contrast focus-visible:hover:tw-border-transparent"
|
||||
[ngClass]="{
|
||||
'tw-cursor-not-allowed': disabled,
|
||||
}"
|
||||
(click)="clear()"
|
||||
>
|
||||
<i class="bwi bwi-close tw-text-xs"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<bit-menu #menu (closed)="handleMenuClosed()">
|
||||
<div
|
||||
*ngIf="renderedOptions"
|
||||
class="tw-max-h-80 tw-min-w-32 tw-max-w-80 tw-text-sm"
|
||||
[ngStyle]="menuWidth && { width: menuWidth + 'px' }"
|
||||
>
|
||||
<ng-container *ngIf="getParent(renderedOptions) as parent">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="viewOption(parent, $event)"
|
||||
class="tw-text-[length:inherit]"
|
||||
[title]="'backTo' | i18n: parent.label ?? placeholderText"
|
||||
>
|
||||
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||
{{ "backTo" | i18n: parent.label ?? placeholderText }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="selectOption(renderedOptions, $event)"
|
||||
[title]="'viewItemsIn' | i18n: renderedOptions.label"
|
||||
class="tw-text-[length:inherit]"
|
||||
>
|
||||
<i slot="start" class="bwi bwi-list" aria-hidden="true"></i>
|
||||
{{ "viewItemsIn" | i18n: renderedOptions.label }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngFor="let option of renderedOptions.children"
|
||||
(click)="option.children?.length ? viewOption(option, $event) : selectOption(option, $event)"
|
||||
[disabled]="option.disabled"
|
||||
[title]="option.label"
|
||||
class="tw-text-[length:inherit]"
|
||||
[attr.aria-haspopup]="option.children?.length ? 'menu' : null"
|
||||
@if (renderedOptions) {
|
||||
<div
|
||||
class="tw-max-h-80 tw-min-w-32 tw-max-w-80 tw-text-sm"
|
||||
[ngStyle]="menuWidth && { width: menuWidth + 'px' }"
|
||||
>
|
||||
<i
|
||||
*ngIf="option.icon"
|
||||
slot="start"
|
||||
class="bwi"
|
||||
[ngClass]="option.icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{ option.label }}
|
||||
<i *ngIf="option.children?.length" slot="end" class="bwi bwi-angle-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
@if (getParent(renderedOptions); as parent) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="viewOption(parent, $event)"
|
||||
class="tw-text-[length:inherit]"
|
||||
[title]="'backTo' | i18n: parent.label ?? placeholderText"
|
||||
>
|
||||
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||
{{ "backTo" | i18n: parent.label ?? placeholderText }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="selectOption(renderedOptions, $event)"
|
||||
[title]="'viewItemsIn' | i18n: renderedOptions.label"
|
||||
class="tw-text-[length:inherit]"
|
||||
>
|
||||
<i slot="start" class="bwi bwi-list" aria-hidden="true"></i>
|
||||
{{ "viewItemsIn" | i18n: renderedOptions.label }}
|
||||
</button>
|
||||
}
|
||||
@for (option of renderedOptions.children; track option) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="
|
||||
option.children?.length ? viewOption(option, $event) : selectOption(option, $event)
|
||||
"
|
||||
[disabled]="option.disabled"
|
||||
[title]="option.label"
|
||||
class="tw-text-[length:inherit]"
|
||||
[attr.aria-haspopup]="option.children?.length ? 'menu' : null"
|
||||
>
|
||||
@if (option.icon) {
|
||||
<i slot="start" class="bwi" [ngClass]="option.icon" aria-hidden="true"></i>
|
||||
}
|
||||
{{ option.label }}
|
||||
@if (option.children?.length) {
|
||||
<i slot="end" class="bwi bwi-angle-right"></i>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</bit-menu>
|
||||
|
||||
@@ -46,6 +46,7 @@ export type ChipSelectOption<T> = Option<T> & {
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
|
||||
@ViewChild(MenuComponent) menu: MenuComponent;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgFor, NgIf } from "@angular/common";
|
||||
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -14,18 +14,16 @@ enum CharacterType {
|
||||
|
||||
@Component({
|
||||
selector: "bit-color-password",
|
||||
template: `<span
|
||||
*ngFor="let character of passwordArray; index as i"
|
||||
[class]="getCharacterClass(character)"
|
||||
>
|
||||
<span>{{ character }}</span>
|
||||
<span *ngIf="showCount" class="tw-whitespace-nowrap tw-text-xs tw-leading-5 tw-text-main">{{
|
||||
i + 1
|
||||
}}</span>
|
||||
</span>`,
|
||||
template: `@for (character of passwordArray; track character; let i = $index) {
|
||||
<span [class]="getCharacterClass(character)">
|
||||
<span>{{ character }}</span>
|
||||
@if (showCount) {
|
||||
<span class="tw-whitespace-nowrap tw-text-xs tw-leading-5 tw-text-main">{{ i + 1 }}</span>
|
||||
}
|
||||
</span>
|
||||
}`,
|
||||
preserveWhitespaces: false,
|
||||
standalone: true,
|
||||
imports: [NgFor, NgIf],
|
||||
})
|
||||
export class ColorPasswordComponent {
|
||||
@Input() password: string = null;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
/**
|
||||
@@ -7,7 +6,6 @@ import { Component } from "@angular/core";
|
||||
@Component({
|
||||
selector: "bit-container",
|
||||
templateUrl: "container.component.html",
|
||||
imports: [CommonModule],
|
||||
standalone: true,
|
||||
})
|
||||
export class ContainerComponent {}
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
class="tw-text-main tw-mb-0 tw-truncate"
|
||||
>
|
||||
{{ title }}
|
||||
<span *ngIf="subtitle" class="tw-text-muted tw-font-normal tw-text-sm">
|
||||
{{ subtitle }}
|
||||
</span>
|
||||
@if (subtitle) {
|
||||
<span class="tw-text-muted tw-font-normal tw-text-sm">
|
||||
{{ subtitle }}
|
||||
</span>
|
||||
}
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
<button
|
||||
@@ -35,9 +37,11 @@
|
||||
'tw-min-h-60': loading,
|
||||
}"
|
||||
>
|
||||
<div *ngIf="loading" class="tw-absolute tw-flex tw-size-full tw-items-center tw-justify-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
|
||||
</div>
|
||||
@if (loading) {
|
||||
<div class="tw-absolute tw-flex tw-size-full tw-items-center tw-justify-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
[ngClass]="{
|
||||
'tw-p-4': !disablePadding,
|
||||
|
||||
@@ -11,16 +11,17 @@
|
||||
{{ acceptButtonText }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="showCancelButton"
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
(click)="dialogRef.close(false)"
|
||||
>
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
@if (showCancelButton) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
(click)="dialogRef.close(false)"
|
||||
>
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { NgIf } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
@@ -39,7 +38,6 @@ const DEFAULT_COLOR: Record<SimpleDialogType, string> = {
|
||||
IconDirective,
|
||||
ButtonComponent,
|
||||
BitFormButtonDirective,
|
||||
NgIf,
|
||||
],
|
||||
})
|
||||
export class SimpleConfigurableDialogComponent {
|
||||
|
||||
@@ -12,23 +12,24 @@ import { DialogModule } from "../../dialog.module";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngFor="let group of dialogs">
|
||||
<h2>{{ group.title }}</h2>
|
||||
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let dialog of group.dialogs"
|
||||
bitButton
|
||||
(click)="openSimpleConfigurableDialog(dialog)"
|
||||
>
|
||||
{{ dialog.title }}
|
||||
</button>
|
||||
@for (group of dialogs; track group) {
|
||||
<div>
|
||||
<h2>{{ group.title }}</h2>
|
||||
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
|
||||
@for (dialog of group.dialogs; track dialog) {
|
||||
<button type="button" bitButton (click)="openSimpleConfigurableDialog(dialog)">
|
||||
{{ dialog.title }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<bit-callout *ngIf="showCallout" [type]="calloutType" title="Dialog Close Result">
|
||||
{{ dialogCloseResult }}
|
||||
</bit-callout>
|
||||
@if (showCallout) {
|
||||
<bit-callout [type]="calloutType" title="Dialog Close Result">
|
||||
{{ dialogCloseResult }}
|
||||
</bit-callout>
|
||||
}
|
||||
`,
|
||||
})
|
||||
class StoryDialogComponent {
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
@fadeIn
|
||||
>
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2 tw-px-4 tw-pt-4 tw-text-center">
|
||||
<ng-container *ngIf="hasIcon; else elseBlock">
|
||||
@if (hasIcon) {
|
||||
<ng-content select="[bitDialogIcon]"></ng-content>
|
||||
</ng-container>
|
||||
<ng-template #elseBlock>
|
||||
} @else {
|
||||
<i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
}
|
||||
<h1
|
||||
bitDialogTitleContainer
|
||||
bitTypography="h3"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NgIf } from "@angular/common";
|
||||
import { Component, ContentChild, Directive } from "@angular/core";
|
||||
|
||||
import { TypographyDirective } from "../../typography/typography.directive";
|
||||
@@ -16,7 +15,7 @@ export class IconDirective {}
|
||||
templateUrl: "./simple-dialog.component.html",
|
||||
animations: [fadeIn],
|
||||
standalone: true,
|
||||
imports: [NgIf, DialogTitleContainerDirective, TypographyDirective],
|
||||
imports: [DialogTitleContainerDirective, TypographyDirective],
|
||||
})
|
||||
export class SimpleDialogComponent {
|
||||
@ContentChild(IconDirective) icon!: IconDirective;
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
>
|
||||
<span bitTypography="body2">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
<span *ngIf="required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
@if (required) {
|
||||
<span class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
}
|
||||
</span>
|
||||
<ng-content select="bit-hint" *ngIf="!hasError"></ng-content>
|
||||
@if (!hasError) {
|
||||
<ng-content select="bit-hint"></ng-content>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
<div *ngIf="hasError" class="tw-mt-1 tw-text-danger tw-text-xs tw-ml-0.5">
|
||||
<i class="bwi bwi-error"></i> {{ displayError }}
|
||||
</div>
|
||||
@if (hasError) {
|
||||
<div class="tw-mt-1 tw-text-danger tw-text-xs tw-ml-0.5">
|
||||
<i class="bwi bwi-error"></i> {{ displayError }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass, NgIf } from "@angular/common";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, ContentChild, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -15,7 +15,7 @@ import { BitFormControlAbstraction } from "./form-control.abstraction";
|
||||
selector: "bit-form-control",
|
||||
templateUrl: "form-control.component.html",
|
||||
standalone: true,
|
||||
imports: [NgClass, TypographyDirective, NgIf, I18nPipe],
|
||||
imports: [NgClass, TypographyDirective, I18nPipe],
|
||||
})
|
||||
export class FormControlComponent {
|
||||
@Input() label: string;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<!-- labels inside a form control (checkbox, radio button) should not truncate -->
|
||||
<span [ngClass]="{ 'tw-truncate': !isInsideFormControl }">
|
||||
<ng-content></ng-content>
|
||||
<ng-container *ngIf="isInsideFormControl">
|
||||
@if (isInsideFormControl) {
|
||||
<ng-container *ngTemplateOutlet="endSlotContent"></ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
</span>
|
||||
<ng-container *ngIf="!isInsideFormControl">
|
||||
@if (!isInsideFormControl) {
|
||||
<ng-container *ngTemplateOutlet="endSlotContent"></ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf } from "@angular/common";
|
||||
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
|
||||
|
||||
@@ -8,15 +8,15 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "bit-error-summary",
|
||||
template: ` <ng-container *ngIf="errorCount > 0">
|
||||
template: ` @if (errorCount > 0) {
|
||||
<i class="bwi bwi-error"></i> {{ "fieldsNeedAttention" | i18n: errorString }}
|
||||
</ng-container>`,
|
||||
}`,
|
||||
host: {
|
||||
class: "tw-block tw-text-danger tw-mt-2",
|
||||
"aria-live": "assertive",
|
||||
},
|
||||
standalone: true,
|
||||
imports: [NgIf, I18nPipe],
|
||||
imports: [I18nPipe],
|
||||
})
|
||||
export class BitErrorSummary {
|
||||
@Input()
|
||||
|
||||
@@ -15,63 +15,65 @@
|
||||
<ng-content select="[bitSuffix]"></ng-content>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="!readOnly; else readOnlyView" class="tw-w-full tw-relative tw-group/bit-form-field">
|
||||
<div class="tw-absolute tw-size-full tw-top-0 tw-pointer-events-none tw-z-20">
|
||||
<div class="tw-size-full tw-flex">
|
||||
<div
|
||||
class="tw-min-w-3 tw-border-r-0 group-focus-within/bit-form-field:tw-border-r-0 !tw-rounded-l-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
<div
|
||||
class="tw-px-1 tw-shrink tw-min-w-0 tw-mt-px tw-border-x-0 tw-border-t-0 group-focus-within/bit-form-field:tw-border-x-0 group-focus-within/bit-form-field:tw-border-t-0 tw-hidden group-has-[bit-label]/bit-form-field:tw-block"
|
||||
[ngClass]="inputBorderClasses"
|
||||
>
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-[0.675rem] tw-mb-0 tw-max-w-full tw-pointer-events-auto"
|
||||
[attr.for]="input.labelForId"
|
||||
@if (!readOnly) {
|
||||
<div class="tw-w-full tw-relative tw-group/bit-form-field">
|
||||
<div class="tw-absolute tw-size-full tw-top-0 tw-pointer-events-none tw-z-20">
|
||||
<div class="tw-size-full tw-flex">
|
||||
<div
|
||||
class="tw-min-w-3 tw-border-r-0 group-focus-within/bit-form-field:tw-border-r-0 !tw-rounded-l-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
<div
|
||||
class="tw-px-1 tw-shrink tw-min-w-0 tw-mt-px tw-border-x-0 tw-border-t-0 group-focus-within/bit-form-field:tw-border-x-0 group-focus-within/bit-form-field:tw-border-t-0 tw-hidden group-has-[bit-label]/bit-form-field:tw-block"
|
||||
[ngClass]="inputBorderClasses"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
|
||||
<span *ngIf="input.required" class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
|
||||
({{ "required" | i18n }})</span
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-[0.675rem] tw-mb-0 tw-max-w-full tw-pointer-events-auto"
|
||||
[attr.for]="input.labelForId"
|
||||
>
|
||||
</label>
|
||||
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
|
||||
@if (input.required) {
|
||||
<span class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
|
||||
({{ "required" | i18n }})</span
|
||||
>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="tw-min-w-3 tw-grow tw-border-l-0 group-focus-within/bit-form-field:tw-border-l-0 !tw-rounded-r-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tw-gap-1 tw-bg-background tw-rounded-lg tw-flex tw-min-h-11 [&:not(:has(button:enabled)):has(input:read-only)]:tw-bg-secondary-100 [&:not(:has(button:enabled)):has(textarea:read-only)]:tw-bg-secondary-100"
|
||||
>
|
||||
<div
|
||||
#prefixContainer
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
|
||||
[hidden]="!prefixHasChildren()"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="tw-min-w-3 tw-grow tw-border-l-0 group-focus-within/bit-form-field:tw-border-l-0 !tw-rounded-r-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
class="default-content tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
|
||||
[ngClass]="[
|
||||
prefixHasChildren() ? '' : 'tw-rounded-l-lg tw-pl-3',
|
||||
suffixHasChildren() ? '' : 'tw-rounded-r-lg tw-pr-3',
|
||||
]"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
#suffixContainer
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
|
||||
[hidden]="!suffixHasChildren()"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tw-gap-1 tw-bg-background tw-rounded-lg tw-flex tw-min-h-11 [&:not(:has(button:enabled)):has(input:read-only)]:tw-bg-secondary-100 [&:not(:has(button:enabled)):has(textarea:read-only)]:tw-bg-secondary-100"
|
||||
>
|
||||
<div
|
||||
#prefixContainer
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
|
||||
[hidden]="!prefixHasChildren()"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="default-content tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
|
||||
[ngClass]="[
|
||||
prefixHasChildren() ? '' : 'tw-rounded-l-lg tw-pl-3',
|
||||
suffixHasChildren() ? '' : 'tw-rounded-r-lg tw-pr-3',
|
||||
]"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
#suffixContainer
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
|
||||
[hidden]="!suffixHasChildren()"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #readOnlyView>
|
||||
} @else {
|
||||
<div class="tw-w-full tw-relative">
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full"
|
||||
@@ -107,9 +109,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-container [ngSwitch]="input.hasError">
|
||||
<ng-content select="bit-hint" *ngSwitchCase="false"></ng-content>
|
||||
<bit-error [error]="input.error" *ngSwitchCase="true"></bit-error>
|
||||
</ng-container>
|
||||
@switch (input.hasError) {
|
||||
@case (false) {
|
||||
<ng-content select="bit-hint"></ng-content>
|
||||
}
|
||||
@case (true) {
|
||||
<bit-error [error]="input.error"></bit-error>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
|
||||
import { NgClass } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -16,7 +17,7 @@ import { TypographyModule } from "../typography";
|
||||
@Component({
|
||||
selector: "bit-item-content, [bit-item-content]",
|
||||
standalone: true,
|
||||
imports: [CommonModule, TypographyModule],
|
||||
imports: [TypographyModule, NgClass],
|
||||
templateUrl: `item-content.component.html`,
|
||||
host: {
|
||||
class:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -14,7 +13,7 @@ import { ItemActionComponent } from "./item-action.component";
|
||||
@Component({
|
||||
selector: "bit-item",
|
||||
standalone: true,
|
||||
imports: [CommonModule, ItemActionComponent],
|
||||
imports: [ItemActionComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "item.component.html",
|
||||
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
|
||||
|
||||
@@ -23,19 +23,21 @@
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- overlay backdrop for side-nav -->
|
||||
<div
|
||||
*ngIf="{
|
||||
@if (
|
||||
{
|
||||
open: sideNavService.open$ | async,
|
||||
} as data"
|
||||
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
|
||||
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
|
||||
>
|
||||
};
|
||||
as data
|
||||
) {
|
||||
<div
|
||||
*ngIf="data.open"
|
||||
(click)="sideNavService.toggle()"
|
||||
class="tw-pointer-events-auto tw-size-full"
|
||||
></div>
|
||||
</div>
|
||||
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
|
||||
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
|
||||
>
|
||||
@if (data.open) {
|
||||
<div (click)="sideNavService.toggle()" class="tw-pointer-events-auto tw-size-full"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
[disabled]="disabled"
|
||||
(click)="clear(item)"
|
||||
>
|
||||
<i *ngIf="item.icon != null" class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
@if (item.icon != null) {
|
||||
<i class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
}
|
||||
<span class="tw-truncate">
|
||||
{{ item.labelName }}
|
||||
</span>
|
||||
@@ -41,10 +43,14 @@
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<div class="tw-flex">
|
||||
<div class="tw-w-7 tw-flex-none">
|
||||
<i *ngIf="isSelected(item)" class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
@if (isSelected(item)) {
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="tw-mr-2 tw-flex-initial">
|
||||
<i *ngIf="item.icon != null" class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
@if (item.icon != null) {
|
||||
<i class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="tw-flex-1">
|
||||
{{ item.listName }}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import { NgIf } from "@angular/common";
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
@@ -39,7 +38,7 @@ let nextId = 0;
|
||||
templateUrl: "./multi-select.component.html",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
|
||||
standalone: true,
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, NgIf, I18nPipe],
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe],
|
||||
})
|
||||
/**
|
||||
* This component has been implemented to only support Multi-select list events
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
<div *ngIf="sideNavService.open$ | async" class="tw-h-px tw-w-full tw-bg-secondary-300"></div>
|
||||
@if (sideNavService.open$ | async) {
|
||||
<div class="tw-h-px tw-w-full tw-bg-secondary-300"></div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- This a higher order component that composes `NavItemComponent` -->
|
||||
<ng-container *ngIf="!hideIfEmpty || nestedNavComponents.length > 0">
|
||||
@if (!hideIfEmpty || nestedNavComponents.length > 0) {
|
||||
<bit-nav-item
|
||||
[text]="text"
|
||||
[icon]="icon"
|
||||
@@ -29,28 +29,29 @@
|
||||
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
|
||||
></button>
|
||||
</ng-template>
|
||||
|
||||
<!-- Show toggle to the left for trees otherwise to the right -->
|
||||
<ng-container slot="start" *ngIf="variant === 'tree'">
|
||||
<ng-container *ngTemplateOutlet="button"></ng-container>
|
||||
</ng-container>
|
||||
<ng-container slot="end">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
<ng-container *ngIf="variant !== 'tree'">
|
||||
@if (variant === "tree") {
|
||||
<ng-container slot="start">
|
||||
<ng-container *ngTemplateOutlet="button"></ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
<ng-container slot="end">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
@if (variant !== "tree") {
|
||||
<ng-container *ngTemplateOutlet="button"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-nav-item>
|
||||
|
||||
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
||||
<ng-container *ngIf="sideNavService.open$ | async">
|
||||
<div
|
||||
*ngIf="open"
|
||||
[attr.id]="contentId"
|
||||
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
|
||||
role="group"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@if (sideNavService.open$ | async) {
|
||||
@if (open) {
|
||||
<div
|
||||
[attr.id]="contentId"
|
||||
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
|
||||
role="group"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SideNavService } from "./side-nav.service";
|
||||
],
|
||||
standalone: true,
|
||||
imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe],
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class NavGroupComponent extends NavBaseComponent implements AfterContentInit {
|
||||
@ContentChildren(NavBaseComponent, {
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<div *ngIf="sideNavService.open" class="tw-sticky tw-top-0 tw-z-50">
|
||||
<a
|
||||
[routerLink]="route"
|
||||
class="tw-px-5 tw-pb-5 tw-pt-7 tw-block tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2"
|
||||
[attr.aria-label]="label"
|
||||
[title]="label"
|
||||
routerLinkActive
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
>
|
||||
<bit-icon [icon]="openIcon"></bit-icon>
|
||||
</a>
|
||||
</div>
|
||||
<bit-nav-item
|
||||
class="tw-block tw-pt-7"
|
||||
[hideActiveStyles]="true"
|
||||
[route]="route"
|
||||
[icon]="closedIcon"
|
||||
*ngIf="!sideNavService.open"
|
||||
[text]="label"
|
||||
></bit-nav-item>
|
||||
@if (sideNavService.open) {
|
||||
<div class="tw-sticky tw-top-0 tw-z-50">
|
||||
<a
|
||||
[routerLink]="route"
|
||||
class="tw-px-5 tw-pb-5 tw-pt-7 tw-block tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2"
|
||||
[attr.aria-label]="label"
|
||||
[title]="label"
|
||||
routerLinkActive
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
>
|
||||
<bit-icon [icon]="openIcon"></bit-icon>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@if (!sideNavService.open) {
|
||||
<bit-nav-item
|
||||
class="tw-block tw-pt-7"
|
||||
[hideActiveStyles]="true"
|
||||
[route]="route"
|
||||
[icon]="closedIcon"
|
||||
[text]="label"
|
||||
></bit-nav-item>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf } from "@angular/common";
|
||||
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { RouterLinkActive, RouterLink } from "@angular/router";
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SideNavService } from "./side-nav.service";
|
||||
selector: "bit-nav-logo",
|
||||
templateUrl: "./nav-logo.component.html",
|
||||
standalone: true,
|
||||
imports: [NgIf, RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent],
|
||||
imports: [RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent],
|
||||
})
|
||||
export class NavLogoComponent {
|
||||
/** Icon that is displayed when the side nav is closed */
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
<nav
|
||||
*ngIf="{
|
||||
@if (
|
||||
{
|
||||
open: sideNavService.open$ | async,
|
||||
isOverlay: sideNavService.isOverlay$ | async,
|
||||
} as data"
|
||||
id="bit-side-nav"
|
||||
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
||||
[ngClass]="{ 'tw-w-60': data.open }"
|
||||
[ngStyle]="
|
||||
variant === 'secondary' && {
|
||||
'--color-text-alt2': 'var(--color-text-main)',
|
||||
'--color-background-alt3': 'var(--color-secondary-100)',
|
||||
'--color-background-alt4': 'var(--color-secondary-300)',
|
||||
}
|
||||
"
|
||||
[cdkTrapFocus]="data.isOverlay"
|
||||
[attr.role]="data.isOverlay ? 'dialog' : null"
|
||||
[attr.aria-modal]="data.isOverlay ? 'true' : null"
|
||||
(keydown)="handleKeyDown($event)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<div class="tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3">
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
<ng-container *ngIf="data.open">
|
||||
<ng-content select="[slot=footer]"></ng-content>
|
||||
</ng-container>
|
||||
<div class="tw-mx-0.5 tw-my-4 tw-w-[3.75rem]">
|
||||
<button
|
||||
#toggleButton
|
||||
type="button"
|
||||
class="tw-mx-auto tw-block tw-max-w-fit"
|
||||
[bitIconButton]="data.open ? 'bwi-angle-left' : 'bwi-angle-right'"
|
||||
buttonType="light"
|
||||
size="small"
|
||||
(click)="sideNavService.toggle()"
|
||||
[attr.aria-label]="'toggleSideNavigation' | i18n"
|
||||
[attr.aria-expanded]="data.open"
|
||||
aria-controls="bit-side-nav"
|
||||
></button>
|
||||
};
|
||||
as data
|
||||
) {
|
||||
<nav
|
||||
id="bit-side-nav"
|
||||
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
||||
[ngClass]="{ 'tw-w-60': data.open }"
|
||||
[ngStyle]="
|
||||
variant === 'secondary' && {
|
||||
'--color-text-alt2': 'var(--color-text-main)',
|
||||
'--color-background-alt3': 'var(--color-secondary-100)',
|
||||
'--color-background-alt4': 'var(--color-secondary-300)',
|
||||
}
|
||||
"
|
||||
[cdkTrapFocus]="data.isOverlay"
|
||||
[attr.role]="data.isOverlay ? 'dialog' : null"
|
||||
[attr.aria-modal]="data.isOverlay ? 'true' : null"
|
||||
(keydown)="handleKeyDown($event)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<div class="tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3">
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
@if (data.open) {
|
||||
<ng-content select="[slot=footer]"></ng-content>
|
||||
}
|
||||
<div class="tw-mx-0.5 tw-my-4 tw-w-[3.75rem]">
|
||||
<button
|
||||
#toggleButton
|
||||
type="button"
|
||||
class="tw-mx-auto tw-block tw-max-w-fit"
|
||||
[bitIconButton]="data.open ? 'bwi-angle-left' : 'bwi-angle-right'"
|
||||
buttonType="light"
|
||||
size="small"
|
||||
(click)="sideNavService.toggle()"
|
||||
[attr.aria-label]="'toggleSideNavigation' | i18n"
|
||||
[attr.aria-expanded]="data.open"
|
||||
aria-controls="bit-side-nav"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
attr.aria-valuenow="{{ barWidth }}"
|
||||
[ngStyle]="{ width: barWidth + '%' }"
|
||||
>
|
||||
<div
|
||||
*ngIf="displayText"
|
||||
class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden"
|
||||
>
|
||||
<!-- If text is too long to fit, wrap it below to hide -->
|
||||
<div class="tw-h-full"> </div>
|
||||
<div class="tw-pr-1">{{ textContent }}</div>
|
||||
</div>
|
||||
@if (displayText) {
|
||||
<div class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden">
|
||||
<!-- If text is too long to fit, wrap it below to hide -->
|
||||
<div class="tw-h-full"> </div>
|
||||
<div class="tw-pr-1">{{ textContent }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<ng-container *ngIf="label">
|
||||
@if (label) {
|
||||
<fieldset>
|
||||
<legend class="tw-mb-1 tw-block tw-text-sm tw-font-semibold tw-text-main">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
<span *ngIf="required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
@if (required) {
|
||||
<span class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
}
|
||||
</legend>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</fieldset>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="!label">
|
||||
@if (!label) {
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-template #content>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf, NgTemplateOutlet } from "@angular/common";
|
||||
import { NgTemplateOutlet } from "@angular/common";
|
||||
import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||
|
||||
@@ -14,7 +14,7 @@ let nextId = 0;
|
||||
selector: "bit-radio-group",
|
||||
templateUrl: "radio-group.component.html",
|
||||
standalone: true,
|
||||
imports: [NgIf, NgTemplateOutlet, I18nPipe],
|
||||
imports: [NgTemplateOutlet, I18nPipe],
|
||||
})
|
||||
export class RadioGroupComponent implements ControlValueAccessor {
|
||||
selected: unknown;
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<div class="tw-flex" [title]="item.label">
|
||||
<div class="tw-mr-2 tw-flex-initial">
|
||||
<i *ngIf="item.icon != null" class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
@if (item.icon != null) {
|
||||
<i class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="tw-flex-1 tw-text-ellipsis tw-overflow-hidden">
|
||||
{{ item.label }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf } from "@angular/common";
|
||||
|
||||
import {
|
||||
Component,
|
||||
ContentChildren,
|
||||
@@ -36,7 +36,7 @@ let nextId = 0;
|
||||
templateUrl: "select.component.html",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
|
||||
standalone: true,
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, NgIf],
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule],
|
||||
})
|
||||
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
|
||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||
|
||||
@@ -49,11 +49,9 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
|
||||
<bit-form-field>
|
||||
<bit-label>Your favorite color</bit-label>
|
||||
<bit-select formControlName="favColor">
|
||||
<bit-option
|
||||
*ngFor="let color of colors"
|
||||
[value]="color.value"
|
||||
[label]="color.name"
|
||||
></bit-option>
|
||||
@for (color of colors; track color) {
|
||||
<bit-option [value]="color.value" [label]="color.name"></bit-option>
|
||||
}
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
|
||||
@@ -36,9 +36,11 @@ class KitchenSinkDialog {
|
||||
|
||||
<p class="tw-mt-4">
|
||||
<bit-breadcrumbs>
|
||||
<bit-breadcrumb *ngFor="let item of navItems" [icon]="item.icon" [route]="[item.route]">
|
||||
{{ item.name }}
|
||||
</bit-breadcrumb>
|
||||
@for (item of navItems; track item) {
|
||||
<bit-breadcrumb [icon]="item.icon" [route]="[item.route]">
|
||||
{{ item.name }}
|
||||
</bit-breadcrumb>
|
||||
}
|
||||
</bit-breadcrumbs>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -16,11 +16,15 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
|
||||
<bit-toggle value="small"> Mid-sized <span bitBadge variant="info">1</span> </bit-toggle>
|
||||
</bit-toggle-group>
|
||||
</div>
|
||||
<ul *ngFor="let company of companyList">
|
||||
<li *ngIf="company.size === selectedToggle || selectedToggle === 'all'">
|
||||
{{ company.name }}
|
||||
</li>
|
||||
</ul>
|
||||
@for (company of companyList; track company) {
|
||||
<ul>
|
||||
@if (company.size === selectedToggle || selectedToggle === "all") {
|
||||
<li>
|
||||
{{ company.name }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class KitchenSinkToggleList {
|
||||
|
||||
@@ -5,40 +5,41 @@
|
||||
[attr.aria-label]="label"
|
||||
(keydown)="keyManager.onKeydown($event)"
|
||||
>
|
||||
<button
|
||||
bitTabListItem
|
||||
*ngFor="let tab of tabs; let i = index"
|
||||
type="button"
|
||||
role="tab"
|
||||
[id]="getTabLabelId(i)"
|
||||
[active]="tab.isActive"
|
||||
[disabled]="tab.disabled"
|
||||
[attr.aria-selected]="selectedIndex === i"
|
||||
[attr.tabindex]="selectedIndex === i ? 0 : -1"
|
||||
(click)="selectTab(i)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="content"></ng-container>
|
||||
|
||||
<ng-template #content>
|
||||
<ng-template [ngIf]="tab.templateLabel" [ngIfElse]="tabTextLabel">
|
||||
<ng-container [ngTemplateOutlet]="tab.templateLabel.templateRef"></ng-container>
|
||||
@for (tab of tabs; track tab; let i = $index) {
|
||||
<button
|
||||
bitTabListItem
|
||||
type="button"
|
||||
role="tab"
|
||||
[id]="getTabLabelId(i)"
|
||||
[active]="tab.isActive"
|
||||
[disabled]="tab.disabled"
|
||||
[attr.aria-selected]="selectedIndex === i"
|
||||
[attr.tabindex]="selectedIndex === i ? 0 : -1"
|
||||
(click)="selectTab(i)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="content"></ng-container>
|
||||
<ng-template #content>
|
||||
@if (tab.templateLabel) {
|
||||
<ng-container [ngTemplateOutlet]="tab.templateLabel.templateRef"></ng-container>
|
||||
} @else {
|
||||
{{ tab.textLabel }}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tabTextLabel>{{ tab.textLabel }}</ng-template>
|
||||
</ng-template>
|
||||
</button>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</bit-tab-header>
|
||||
<div class="tw-px-4 tw-pt-5">
|
||||
<bit-tab-body
|
||||
role="tabpanel"
|
||||
*ngFor="let tab of tabs; let i = index"
|
||||
[id]="getTabContentId(i)"
|
||||
[attr.tabindex]="tab.contentTabIndex"
|
||||
[attr.labeledby]="getTabLabelId(i)"
|
||||
[active]="tab.isActive"
|
||||
[content]="tab.content"
|
||||
[preserveContent]="preserveContent"
|
||||
>
|
||||
</bit-tab-body>
|
||||
@for (tab of tabs; track tab; let i = $index) {
|
||||
<bit-tab-body
|
||||
role="tabpanel"
|
||||
[id]="getTabContentId(i)"
|
||||
[attr.tabindex]="tab.contentTabIndex"
|
||||
[attr.labeledby]="getTabLabelId(i)"
|
||||
[active]="tab.isActive"
|
||||
[content]="tab.content"
|
||||
[preserveContent]="preserveContent"
|
||||
>
|
||||
</bit-tab-body>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { FocusKeyManager } from "@angular/cdk/a11y";
|
||||
import { coerceNumberProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgTemplateOutlet } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
@@ -33,7 +33,7 @@ let nextId = 0;
|
||||
templateUrl: "./tab-group.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgTemplateOutlet,
|
||||
TabHeaderComponent,
|
||||
TabListContainerDirective,
|
||||
TabListItemDirective,
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
<i aria-hidden="true" class="bwi tw-text-xl tw-py-1.5 tw-px-2.5 {{ iconClass }}"></i>
|
||||
<div>
|
||||
<span class="tw-sr-only">{{ variant | i18n }}</span>
|
||||
<p *ngIf="title" data-testid="toast-title" class="tw-font-semibold tw-mb-0">{{ title }}</p>
|
||||
<p
|
||||
bitTypography="body2"
|
||||
*ngFor="let m of messageArray"
|
||||
data-testid="toast-message"
|
||||
class="tw-mb-2 last:tw-mb-0"
|
||||
>
|
||||
{{ m }}
|
||||
</p>
|
||||
@if (title) {
|
||||
<p data-testid="toast-title" class="tw-font-semibold tw-mb-0">{{ title }}</p>
|
||||
}
|
||||
@for (m of messageArray; track m) {
|
||||
<p bitTypography="body2" data-testid="toast-message" class="tw-mb-2 last:tw-mb-0">
|
||||
{{ m }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
|
||||
<button
|
||||
|
||||
2
libs/eslint/empty.ts
Normal file
2
libs/eslint/empty.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// This file is used to avoid TS errors. This package only uses `tsconfig.json` for dynamically generated test files but
|
||||
// TS doesn't know that in the CI.
|
||||
10
libs/eslint/jest.config.js
Normal file
10
libs/eslint/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
testMatch: ["**/+(*.)+(spec).+(mjs)"],
|
||||
displayName: "libs/eslint tests",
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.mjs"],
|
||||
};
|
||||
3
libs/eslint/platform/index.mjs
Normal file
3
libs/eslint/platform/index.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import requiredUsing from "./required-using.mjs";
|
||||
|
||||
export default { rules: { "required-using": requiredUsing } };
|
||||
83
libs/eslint/platform/required-using.mjs
Normal file
83
libs/eslint/platform/required-using.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
|
||||
export const errorMessage = "'using' keyword is required but not used";
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Ensure objects implementing UsingRequired are used with the using keyword",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
const parserServices = ESLintUtils.getParserServices(context);
|
||||
const checker = parserServices.program.getTypeChecker();
|
||||
|
||||
// Function to check if a type implements the `UsingRequired` interface
|
||||
function implementsUsingRequired(type) {
|
||||
const symbol = type.getSymbol();
|
||||
if (!symbol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const declarations = symbol.getDeclarations() || [];
|
||||
for (const declaration of declarations) {
|
||||
const heritageClauses = declaration.heritageClauses || [];
|
||||
for (const clause of heritageClauses) {
|
||||
if (
|
||||
clause.types.some(
|
||||
(typeExpression) =>
|
||||
checker.typeToString(checker.getTypeAtLocation(typeExpression.expression)) ===
|
||||
"UsingRequired",
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to check if a function call returns a `UsingRequired`
|
||||
function returnsUsingRequired(node) {
|
||||
if (node.type === "CallExpression") {
|
||||
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
||||
const returnType = checker.getTypeAtLocation(tsNode);
|
||||
|
||||
return implementsUsingRequired(returnType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
VariableDeclarator(node) {
|
||||
// Skip if `using` is already present
|
||||
if (node.parent.type === "VariableDeclaration" && node.parent.kind === "using") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the initializer returns a `UsingRequired`
|
||||
if (node.init && returnsUsingRequired(node.init)) {
|
||||
context.report({
|
||||
node,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
},
|
||||
AssignmentExpression(node) {
|
||||
// Check if the right-hand side returns a `UsingRequired`
|
||||
if (returnsUsingRequired(node.right)) {
|
||||
context.report({
|
||||
node,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
98
libs/eslint/platform/required-using.spec.mjs
Normal file
98
libs/eslint/platform/required-using.spec.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import rule, { errorMessage } from "./required-using.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: [__dirname + "/../tsconfig.spec.json"],
|
||||
projectService: {
|
||||
allowDefaultProject: ["*.ts*"],
|
||||
},
|
||||
tsconfigRootDir: __dirname + "/..",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const setup = `
|
||||
interface UsingRequired {}
|
||||
class Ref implements UsingRequired {}
|
||||
|
||||
const rc = {
|
||||
take(): Ref {
|
||||
return new Ref();
|
||||
},
|
||||
};
|
||||
`;
|
||||
|
||||
ruleTester.run("required-using", rule.default, {
|
||||
valid: [
|
||||
{
|
||||
name: "Direct declaration with `using`",
|
||||
code: `
|
||||
${setup}
|
||||
using client = rc.take();
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Function reference with `using`",
|
||||
code: `
|
||||
${setup}
|
||||
const t = rc.take;
|
||||
using client = t();
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: "Direct declaration without `using`",
|
||||
code: `
|
||||
${setup}
|
||||
const client = rc.take();
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Assignment without `using`",
|
||||
code: `
|
||||
${setup}
|
||||
let client;
|
||||
client = rc.take();
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Function reference without `using`",
|
||||
code: `
|
||||
${setup}
|
||||
const t = rc.take;
|
||||
const client = t();
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Destructuring without `using`",
|
||||
code: `
|
||||
${setup}
|
||||
const { value } = rc.take();
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
8
libs/eslint/test.setup.mjs
Normal file
8
libs/eslint/test.setup.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
import { clearImmediate, setImmediate } from "node:timers";
|
||||
|
||||
Object.defineProperties(globalThis, {
|
||||
clearImmediate: { value: clearImmediate },
|
||||
setImmediate: { value: setImmediate },
|
||||
});
|
||||
5
libs/eslint/tsconfig.json
Normal file
5
libs/eslint/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../shared/tsconfig",
|
||||
"compilerOptions": {},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
libs/eslint/tsconfig.spec.json
Normal file
3
libs/eslint/tsconfig.spec.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./security-task";
|
||||
export * from "./security-task.data";
|
||||
export * from "./security-task.response";
|
||||
|
||||
8881
package-lock.json
generated
8881
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,8 @@
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
"@types/retry": "0.12.5",
|
||||
"@types/zxcvbn": "4.4.5",
|
||||
"@typescript-eslint/rule-tester": "8.22.0",
|
||||
"@typescript-eslint/utils": "8.22.0",
|
||||
"@webcomponents/custom-elements": "1.6.0",
|
||||
"@yao-pkg/pkg": "5.16.1",
|
||||
"angular-eslint": "18.4.3",
|
||||
|
||||
Reference in New Issue
Block a user