mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
Merge branch 'master' into feature/org-admin-refresh
This commit is contained in:
17
.github/workflows/staged-rollout-desktop.yml
vendored
17
.github/workflows/staged-rollout-desktop.yml
vendored
@@ -15,26 +15,9 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
|
||||||
name: Setup
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
|
|
||||||
|
|
||||||
- name: Branch check
|
|
||||||
run: |
|
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then
|
|
||||||
echo "==================================="
|
|
||||||
echo "[!] Can only increase rollout from the 'rc' or 'hotfix-rc-desktop' branches"
|
|
||||||
echo "==================================="
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rollout:
|
rollout:
|
||||||
name: Update Rollout Percentage
|
name: Update Rollout Percentage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: setup
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure
|
- name: Login to Azure
|
||||||
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
|
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2022.12.0",
|
"version": "2022.12.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
|
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2022.12.0",
|
"version": "2022.12.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"minimum_chrome_version": "102.0",
|
"minimum_chrome_version": "102.0",
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2022.12.0",
|
"version": "2022.12.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
|||||||
|
|
||||||
import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component";
|
import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||||
|
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||||
|
|
||||||
import { EnvironmentComponent } from "./accounts/environment.component";
|
import { EnvironmentComponent } from "./accounts/environment.component";
|
||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
@@ -202,6 +204,8 @@ registerLocaleData(localeZhTw, "zh-TW");
|
|||||||
CipherRowComponent,
|
CipherRowComponent,
|
||||||
VaultItemsComponent,
|
VaultItemsComponent,
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
|
ColorPasswordPipe,
|
||||||
|
ColorPasswordCountPipe,
|
||||||
CurrentTabComponent,
|
CurrentTabComponent,
|
||||||
EnvironmentComponent,
|
EnvironmentComponent,
|
||||||
ExcludedDomainsComponent,
|
ExcludedDomainsComponent,
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ p.lead {
|
|||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
|
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
color: themed("mutedColor") !important;
|
color: themed("passwordCountText") !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ $themes: (
|
|||||||
logoSuffix: "dark",
|
logoSuffix: "dark",
|
||||||
passwordNumberColor: #007fde,
|
passwordNumberColor: #007fde,
|
||||||
passwordSpecialColor: #c40800,
|
passwordSpecialColor: #c40800,
|
||||||
|
passwordCountText: #212529,
|
||||||
calloutBorderColor: $border-color-dark,
|
calloutBorderColor: $border-color-dark,
|
||||||
calloutBackgroundColor: $box-background-color,
|
calloutBackgroundColor: $box-background-color,
|
||||||
toastTextColor: #ffffff,
|
toastTextColor: #ffffff,
|
||||||
@@ -170,6 +171,7 @@ $themes: (
|
|||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
passwordNumberColor: #6f9df1,
|
passwordNumberColor: #6f9df1,
|
||||||
passwordSpecialColor: #ff8d85,
|
passwordSpecialColor: #ff8d85,
|
||||||
|
passwordCountText: #ffffff,
|
||||||
calloutBorderColor: #4c525f,
|
calloutBorderColor: #4c525f,
|
||||||
calloutBackgroundColor: #3c424e,
|
calloutBackgroundColor: #3c424e,
|
||||||
toastTextColor: #1f242e,
|
toastTextColor: #1f242e,
|
||||||
@@ -230,6 +232,7 @@ $themes: (
|
|||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
passwordNumberColor: $nord8,
|
passwordNumberColor: $nord8,
|
||||||
passwordSpecialColor: $nord12,
|
passwordSpecialColor: $nord12,
|
||||||
|
passwordCountText: $nord5,
|
||||||
calloutBorderColor: $nord0,
|
calloutBorderColor: $nord0,
|
||||||
calloutBackgroundColor: $nord2,
|
calloutBackgroundColor: $nord2,
|
||||||
toastTextColor: #ffffff,
|
toastTextColor: #ffffff,
|
||||||
@@ -290,6 +293,7 @@ $themes: (
|
|||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
passwordNumberColor: $solarizedDarkCyan,
|
passwordNumberColor: $solarizedDarkCyan,
|
||||||
passwordSpecialColor: $solarizedDarkYellow,
|
passwordSpecialColor: $solarizedDarkYellow,
|
||||||
|
passwordCountText: $solarizedDarkBase2,
|
||||||
calloutBorderColor: $solarizedDarkBase03,
|
calloutBorderColor: $solarizedDarkBase03,
|
||||||
calloutBackgroundColor: $solarizedDarkBase01,
|
calloutBackgroundColor: $solarizedDarkBase01,
|
||||||
toastTextColor: #ffffff,
|
toastTextColor: #ffffff,
|
||||||
|
|||||||
@@ -13,9 +13,10 @@
|
|||||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<div class="row-main-content">
|
<div class="row-main-content">
|
||||||
<span class="text monospaced no-ellipsis">
|
<span
|
||||||
{{ h.password }}
|
class="text monospaced no-ellipsis"
|
||||||
</span>
|
[innerHTML]="h.password | colorPassword"
|
||||||
|
></span>
|
||||||
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ import localeZhCn from "@angular/common/locales/zh-Hans";
|
|||||||
import localeZhTw from "@angular/common/locales/zh-Hant";
|
import localeZhTw from "@angular/common/locales/zh-Hant";
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||||
|
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||||
|
|
||||||
import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component";
|
import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component";
|
||||||
import { DeleteAccountComponent } from "./accounts/delete-account.component";
|
import { DeleteAccountComponent } from "./accounts/delete-account.component";
|
||||||
import { EnvironmentComponent } from "./accounts/environment.component";
|
import { EnvironmentComponent } from "./accounts/environment.component";
|
||||||
@@ -170,6 +173,8 @@ registerLocaleData(localeZhTw, "zh-TW");
|
|||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
VaultItemsComponent,
|
VaultItemsComponent,
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
|
ColorPasswordPipe,
|
||||||
|
ColorPasswordCountPipe,
|
||||||
DeleteAccountComponent,
|
DeleteAccountComponent,
|
||||||
EnvironmentComponent,
|
EnvironmentComponent,
|
||||||
ExportComponent,
|
ExportComponent,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<div
|
<div
|
||||||
class="generated-wrapper monospaced"
|
class="password-wrapper monospaced"
|
||||||
appSelectCopy
|
appSelectCopy
|
||||||
[innerHTML]="h.password | colorPassword"
|
[innerHTML]="h.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
<div class="box-content condensed">
|
<div class="box-content condensed">
|
||||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<span class="text monospaced">
|
<span class="text monospaced" [innerHTML]="h.password | colorPassword"></span>
|
||||||
{{ h.password }}
|
|
||||||
</span>
|
|
||||||
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
|
|||||||
@@ -17,12 +17,16 @@
|
|||||||
{{ field.value || " " }}
|
{{ field.value || " " }}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="field.type === fieldType.Hidden">
|
<div *ngIf="field.type === fieldType.Hidden">
|
||||||
|
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
|
||||||
<span
|
<span
|
||||||
*ngIf="field.showValue"
|
*ngIf="field.showValue && !field.showCount"
|
||||||
class="monospaced show-whitespace"
|
class="monospaced show-whitespace"
|
||||||
[innerHTML]="field.value | colorPassword"
|
[innerHTML]="field.value | colorPassword"
|
||||||
></span>
|
></span>
|
||||||
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
|
<span
|
||||||
|
*ngIf="field.showValue && field.showCount"
|
||||||
|
[innerHTML]="field.value | colorPasswordCount"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="field.type === fieldType.Boolean">
|
<div *ngIf="field.type === fieldType.Boolean">
|
||||||
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
||||||
@@ -41,7 +45,18 @@
|
|||||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons action-buttons-fixed">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="row-btn"
|
||||||
|
appStopClick
|
||||||
|
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||||
|
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
|
||||||
|
(click)="toggleFieldCount(field)"
|
||||||
|
[attr.aria-pressed]="field.showCount"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="row-btn"
|
class="row-btn"
|
||||||
|
|||||||
@@ -50,11 +50,15 @@
|
|||||||
{{ cipher.login.maskedPassword }}
|
{{ cipher.login.maskedPassword }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="showPassword"
|
*ngIf="showPassword && !showPasswordCount"
|
||||||
class="monospaced generated-wrapper"
|
class="monospaced password-wrapper"
|
||||||
appSelectCopy
|
appSelectCopy
|
||||||
[innerHTML]="cipher.login.password | colorPassword"
|
[innerHTML]="cipher.login.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
|
<div
|
||||||
|
*ngIf="showPassword && showPasswordCount"
|
||||||
|
[innerHTML]="cipher.login.password | colorPasswordCount"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons" *ngIf="cipher.viewPassword">
|
<div class="action-buttons" *ngIf="cipher.viewPassword">
|
||||||
<button
|
<button
|
||||||
@@ -77,6 +81,18 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="row-btn"
|
||||||
|
appStopClick
|
||||||
|
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
|
||||||
|
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||||
|
(click)="togglePasswordCount()"
|
||||||
|
*ngIf="showPassword"
|
||||||
|
[attr.aria-pressed]="showPasswordCount"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="row-btn"
|
class="row-btn"
|
||||||
|
|||||||
@@ -2057,5 +2057,9 @@
|
|||||||
},
|
},
|
||||||
"logInWithAnotherDevice": {
|
"logInWithAnotherDevice": {
|
||||||
"message": "Log in with another device"
|
"message": "Log in with another device"
|
||||||
|
},
|
||||||
|
"toggleCharacterCount": {
|
||||||
|
"message": "Toggle character count",
|
||||||
|
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|
||||||
|
&.action-buttons-fixed {
|
||||||
|
align-self: start;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.row-btn {
|
.row-btn {
|
||||||
@extend .icon-btn;
|
@extend .icon-btn;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ p.lead {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.generated-wrapper {
|
.password-wrapper {
|
||||||
word-break: break-all;
|
overflow-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -233,6 +233,30 @@ p.lead {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-character {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
@include themify($themes) {
|
||||||
|
background-color: themed("backgroundColorAlt2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-count {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 8px;
|
||||||
|
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("passwordCountText") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#duo-frame {
|
#duo-frame {
|
||||||
background: url("../images/loading.svg") 0 0 no-repeat;
|
background: url("../images/loading.svg") 0 0 no-repeat;
|
||||||
height: 330px;
|
height: 330px;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ $themes: (
|
|||||||
logoSuffix: "dark",
|
logoSuffix: "dark",
|
||||||
passwordNumberColor: #007fde,
|
passwordNumberColor: #007fde,
|
||||||
passwordSpecialColor: #c40800,
|
passwordSpecialColor: #c40800,
|
||||||
|
passwordCountText: #212529,
|
||||||
calloutBorderColor: $border-color-dark,
|
calloutBorderColor: $border-color-dark,
|
||||||
calloutBackgroundColor: $background-color,
|
calloutBackgroundColor: $background-color,
|
||||||
accountSwitcherBackgroundColor: $background-color,
|
accountSwitcherBackgroundColor: $background-color,
|
||||||
@@ -142,6 +143,7 @@ $themes: (
|
|||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
passwordNumberColor: #52bdfb,
|
passwordNumberColor: #52bdfb,
|
||||||
passwordSpecialColor: #ff7c70,
|
passwordSpecialColor: #ff7c70,
|
||||||
|
passwordCountText: #ffffff,
|
||||||
calloutBorderColor: #2f2f2f,
|
calloutBorderColor: #2f2f2f,
|
||||||
calloutBackgroundColor: #363636,
|
calloutBackgroundColor: #363636,
|
||||||
accountSwitcherBackgroundColor: #2f2f2f,
|
accountSwitcherBackgroundColor: #2f2f2f,
|
||||||
@@ -196,6 +198,7 @@ $themes: (
|
|||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
passwordNumberColor: $nord8,
|
passwordNumberColor: $nord8,
|
||||||
passwordSpecialColor: $nord12,
|
passwordSpecialColor: $nord12,
|
||||||
|
passwordCountText: $nord5,
|
||||||
calloutBorderColor: $nord1,
|
calloutBorderColor: $nord1,
|
||||||
calloutBackgroundColor: $nord2,
|
calloutBackgroundColor: $nord2,
|
||||||
accountSwitcherBackgroundColor: $nord0,
|
accountSwitcherBackgroundColor: $nord0,
|
||||||
|
|||||||
@@ -77,18 +77,12 @@
|
|||||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="login_input_master-password"
|
id="login_input_master-password"
|
||||||
|
type="password"
|
||||||
bitInput
|
bitInput
|
||||||
[type]="showPassword ? 'text' : 'password'"
|
|
||||||
formControlName="masterPassword"
|
formControlName="masterPassword"
|
||||||
appAutofocus
|
appAutofocus
|
||||||
/>
|
/>
|
||||||
<button type="button" bitSuffix bitButton (click)="togglePassword()">
|
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
|
||||||
<i
|
|
||||||
aria-hidden="true"
|
|
||||||
class="bwi bwi-lg bwi-eye"
|
|
||||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<a class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" (click)="setFormValues()">{{
|
<a class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" (click)="setFormValues()">{{
|
||||||
"getMasterPasswordHint" | i18n
|
"getMasterPasswordHint" | i18n
|
||||||
|
|||||||
@@ -34,16 +34,16 @@
|
|||||||
<input
|
<input
|
||||||
id="register-form_input_master-password"
|
id="register-form_input_master-password"
|
||||||
bitInput
|
bitInput
|
||||||
[type]="showPassword ? 'text' : 'password'"
|
type="password"
|
||||||
formControlName="masterPassword"
|
formControlName="masterPassword"
|
||||||
/>
|
/>
|
||||||
<button type="button" bitSuffix bitButton (click)="togglePassword()">
|
<button
|
||||||
<i
|
type="button"
|
||||||
aria-hidden="true"
|
bitSuffix
|
||||||
class="bwi bwi-lg bwi-eye"
|
bitIconButton
|
||||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
bitPasswordInputToggle
|
||||||
></i>
|
[(toggled)]="showPassword"
|
||||||
</button>
|
></button>
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
<span class="tw-font-semibold">Important:</span>
|
<span class="tw-font-semibold">Important:</span>
|
||||||
{{ "masterPassImportant" | i18n }}
|
{{ "masterPassImportant" | i18n }}
|
||||||
@@ -65,16 +65,16 @@
|
|||||||
<input
|
<input
|
||||||
id="register-form_input_confirm-master-password"
|
id="register-form_input_confirm-master-password"
|
||||||
bitInput
|
bitInput
|
||||||
[type]="showPassword ? 'text' : 'password'"
|
type="password"
|
||||||
formControlName="confirmMasterPassword"
|
formControlName="confirmMasterPassword"
|
||||||
/>
|
/>
|
||||||
<button type="button" bitSuffix bitButton (click)="togglePassword()">
|
<button
|
||||||
<i
|
type="button"
|
||||||
aria-hidden="true"
|
bitSuffix
|
||||||
class="bwi bwi-lg bwi-eye"
|
bitIconButton
|
||||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
bitPasswordInputToggle
|
||||||
></i>
|
[(toggled)]="showPassword"
|
||||||
</button>
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export class PeopleComponent
|
|||||||
if (
|
if (
|
||||||
!user &&
|
!user &&
|
||||||
this.organization.planProductType === ProductType.Free &&
|
this.organization.planProductType === ProductType.Free &&
|
||||||
this.users.length === this.organization.seats
|
this.allUsers.length === this.organization.seats
|
||||||
) {
|
) {
|
||||||
// Show org upgrade modal
|
// Show org upgrade modal
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
|
ColorPasswordModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
// Register the locales for the application
|
// Register the locales for the application
|
||||||
@@ -66,6 +67,8 @@ import "./locales";
|
|||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
|
LinkModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
|
||||||
// Web specific
|
// Web specific
|
||||||
],
|
],
|
||||||
@@ -97,6 +100,8 @@ import "./locales";
|
|||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
|
LinkModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
|
||||||
// Web specific
|
// Web specific
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,18 +6,10 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<div class="card card-generated bg-light my-4">
|
<div class="card card-generated bg-light my-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div
|
<bit-color-password
|
||||||
*ngIf="type === 'password'"
|
[password]="type === 'password' ? password : username"
|
||||||
class="generated-wrapper"
|
|
||||||
[innerHTML]="password | colorPassword"
|
|
||||||
appSelectCopy
|
appSelectCopy
|
||||||
></div>
|
></bit-color-password>
|
||||||
<div
|
|
||||||
*ngIf="type === 'username'"
|
|
||||||
class="generated-wrapper"
|
|
||||||
[innerHTML]="username | colorPassword"
|
|
||||||
appSelectCopy
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading">
|
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading">
|
||||||
|
|||||||
@@ -84,73 +84,41 @@
|
|||||||
<br />
|
<br />
|
||||||
|
|
||||||
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
|
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
|
||||||
<div class="input-group">
|
<bit-form-field>
|
||||||
<bit-form-field class="tw-w-full">
|
<bit-label>{{ "filePassword" | i18n }}</bit-label>
|
||||||
<bit-label>{{ "filePassword" | i18n }}</bit-label>
|
<input
|
||||||
<input
|
bitInput
|
||||||
bitInput
|
type="password"
|
||||||
[type]="showFilePassword ? 'text' : 'password'"
|
id="filePassword"
|
||||||
id="filePassword"
|
formControlName="filePassword"
|
||||||
formControlName="filePassword"
|
name="password"
|
||||||
name="password"
|
/>
|
||||||
/>
|
<button
|
||||||
|
type="button"
|
||||||
<div class="input-group-append">
|
bitSuffix
|
||||||
<button
|
bitIconButton
|
||||||
bitSuffix
|
bitPasswordInputToggle
|
||||||
bitButton
|
[(toggled)]="showFilePassword"
|
||||||
buttonType="secondary"
|
></button>
|
||||||
appStopClick
|
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
</bit-form-field>
|
||||||
[attr.aria-pressed]="showFilePassword"
|
<bit-form-field>
|
||||||
(click)="toggleFilePassword()"
|
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
||||||
type="button"
|
<input
|
||||||
>
|
bitInput
|
||||||
<i
|
type="password"
|
||||||
class="bwi bwi-lg"
|
id="confirmFilePassword"
|
||||||
aria-hidden="true"
|
formControlName="confirmFilePassword"
|
||||||
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }"
|
name="confirmFilePassword"
|
||||||
></i>
|
/>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
</bit-form-field>
|
bitSuffix
|
||||||
<div class="small text-muted">
|
bitIconButton
|
||||||
{{ "exportPasswordDescription" | i18n }}
|
bitPasswordInputToggle
|
||||||
</div>
|
[(toggled)]="showFilePassword"
|
||||||
</div>
|
></button>
|
||||||
<div class="input-group tw-mt-4">
|
</bit-form-field>
|
||||||
<bit-form-field class="tw-w-full">
|
|
||||||
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
|
||||||
<input
|
|
||||||
bitInput
|
|
||||||
[type]="showConfirmFilePassword ? 'text' : 'password'"
|
|
||||||
id="confirmFilePassword"
|
|
||||||
formControlName="confirmFilePassword"
|
|
||||||
name="confirmFilePassword"
|
|
||||||
/>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button
|
|
||||||
bitSuffix
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
appStopClick
|
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
|
||||||
[attr.aria-pressed]="showConfirmFilePassword"
|
|
||||||
(click)="toggleConfirmFilePassword()"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-eye': !showConfirmFilePassword,
|
|
||||||
'bwi-eye-slash': showConfirmFilePassword
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { UserVerificationPromptComponent } from "../../components/user-verificat
|
|||||||
export class ExportComponent extends BaseExportComponent {
|
export class ExportComponent extends BaseExportComponent {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
encryptedExportType = EncryptedExportType;
|
encryptedExportType = EncryptedExportType;
|
||||||
|
protected showFilePassword: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
|
|||||||
@@ -14,32 +14,17 @@
|
|||||||
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5"
|
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5"
|
||||||
>
|
>
|
||||||
{{ "confirmVaultImportDesc" | i18n }}
|
{{ "confirmVaultImportDesc" | i18n }}
|
||||||
<bit-form-field class="tw-w-full tw-pt-3.5">
|
<bit-form-field class="tw-pt-3.5">
|
||||||
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
bitInput
|
bitInput
|
||||||
required
|
type="password"
|
||||||
[type]="showFilePassword ? 'text' : 'password'"
|
|
||||||
name="filePassword"
|
name="filePassword"
|
||||||
[formControl]="filePassword"
|
[formControl]="filePassword"
|
||||||
appAutofocus
|
appAutofocus
|
||||||
appInputVerbatim
|
appInputVerbatim
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
|
||||||
bitSuffix
|
|
||||||
bitButton
|
|
||||||
appStopClick
|
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
|
||||||
[attr.aria-pressed]="showFilePassword"
|
|
||||||
(click)="toggleFilePassword()"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,15 +7,10 @@ import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
|||||||
templateUrl: "file-password-prompt.component.html",
|
templateUrl: "file-password-prompt.component.html",
|
||||||
})
|
})
|
||||||
export class FilePasswordPromptComponent {
|
export class FilePasswordPromptComponent {
|
||||||
showFilePassword: boolean;
|
|
||||||
filePassword = new FormControl("", Validators.required);
|
filePassword = new FormControl("", Validators.required);
|
||||||
|
|
||||||
constructor(private modalRef: ModalRef) {}
|
constructor(private modalRef: ModalRef) {}
|
||||||
|
|
||||||
toggleFilePassword() {
|
|
||||||
this.showFilePassword = !this.showFilePassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.filePassword.markAsTouched();
|
this.filePassword.markAsTouched();
|
||||||
if (!this.filePassword.valid) {
|
if (!this.filePassword.valid) {
|
||||||
|
|||||||
@@ -288,6 +288,10 @@
|
|||||||
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
|
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
|
||||||
CSV file.
|
CSV file.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="format === 'passkyjson'">
|
||||||
|
Log in to "https://vault.passky.org" → "Import & Export" → "Export" in the Passky
|
||||||
|
section. ("Backup" is unsupported as it is encrypted).
|
||||||
|
</ng-container>
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
<div class="modal-body" *ngIf="history.length">
|
<div class="modal-body" *ngIf="history.length">
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item d-flex" *ngFor="let h of history">
|
<li class="list-group-item d-flex" *ngFor="let h of history">
|
||||||
<div class="password-row">
|
<div class="tw-min-w-0">
|
||||||
<div
|
<bit-color-password
|
||||||
class="text-monospace generated-wrapper"
|
[password]="h.password"
|
||||||
[innerHTML]="h.password | colorPassword"
|
class="tw-block tw-font-mono"
|
||||||
appSelectCopy
|
appSelectCopy
|
||||||
></div>
|
></bit-color-password>
|
||||||
<small class="text-muted">{{ h.date | date: "medium" }}</small>
|
<small class="text-muted">{{ h.date | date: "medium" }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
|
|||||||
@@ -128,6 +128,15 @@
|
|||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
></i>
|
></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="d-block bwi-icon-above-input"
|
||||||
|
appStopClick
|
||||||
|
[appA11yTitle]="'toggleCharacterCount' | i18n"
|
||||||
|
(click)="togglePasswordCount()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-fw bwi-numbered-list" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -169,6 +178,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="showPasswordCount" class="tw-mb-4">
|
||||||
|
<label>{{ "passwordCharacterCount" | i18n }}</label>
|
||||||
|
<div class="tw-flex tw-justify-between">
|
||||||
|
<bit-color-password
|
||||||
|
[password]="cipher.login.password"
|
||||||
|
[showCount]="true"
|
||||||
|
></bit-color-password>
|
||||||
|
<button type="button" bitLink (click)="togglePasswordCount()">
|
||||||
|
{{ "hide" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tw-flex tw-flex-row">
|
<div class="tw-flex tw-flex-row">
|
||||||
<div class="tw-mb-4 tw-w-1/2">
|
<div class="tw-mb-4 tw-w-1/2">
|
||||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||||
@@ -870,7 +891,7 @@
|
|||||||
<div class="ml-3" *ngIf="viewingPasswordHistory">
|
<div class="ml-3" *ngIf="viewingPasswordHistory">
|
||||||
<div *ngFor="let ph of cipher.passwordHistory">
|
<div *ngFor="let ph of cipher.passwordHistory">
|
||||||
{{ ph.lastUsedDate | date: "short" }} -
|
{{ ph.lastUsedDate | date: "short" }} -
|
||||||
<span class="generated-wrapper text-monospace ml-2">{{ ph.password }}</span>
|
<bit-color-password [password]="ph.password"></bit-color-password>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
hasPasswordHistory = false;
|
hasPasswordHistory = false;
|
||||||
viewingPasswordHistory = false;
|
viewingPasswordHistory = false;
|
||||||
viewOnly = false;
|
viewOnly = false;
|
||||||
|
showPasswordCount = false;
|
||||||
|
|
||||||
protected totpInterval: number;
|
protected totpInterval: number;
|
||||||
protected override componentName = "app-vault-add-edit";
|
protected override componentName = "app-vault-add-edit";
|
||||||
@@ -104,6 +105,26 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
this.cipher.favorite = !this.cipher.favorite;
|
this.cipher.favorite = !this.cipher.favorite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePassword() {
|
||||||
|
super.togglePassword();
|
||||||
|
|
||||||
|
// Hide password count when password is hidden to be safe
|
||||||
|
if (!this.showPassword && this.showPasswordCount) {
|
||||||
|
this.togglePasswordCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePasswordCount() {
|
||||||
|
this.showPasswordCount = !this.showPasswordCount;
|
||||||
|
|
||||||
|
if (this.editMode && this.showPasswordCount) {
|
||||||
|
this.eventCollectionService.collect(
|
||||||
|
EventType.Cipher_ClientToggledPasswordVisible,
|
||||||
|
this.cipherId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
launch(uri: LoginUriView) {
|
launch(uri: LoginUriView) {
|
||||||
if (!uri.canLaunch) {
|
if (!uri.canLaunch) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5579,6 +5579,17 @@
|
|||||||
"multiSelectClearAll": {
|
"multiSelectClearAll": {
|
||||||
"message": "Clear all"
|
"message": "Clear all"
|
||||||
},
|
},
|
||||||
|
"toggleCharacterCount": {
|
||||||
|
"message": "Toggle character count",
|
||||||
|
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
||||||
|
},
|
||||||
|
"passwordCharacterCount": {
|
||||||
|
"message": "Password character count",
|
||||||
|
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
||||||
|
},
|
||||||
|
"hide": {
|
||||||
|
"message": "Hide"
|
||||||
|
},
|
||||||
"projects":{
|
"projects":{
|
||||||
"message": "Projects"
|
"message": "Projects"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,31 +1,3 @@
|
|||||||
.generated-wrapper {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-row {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-letter {
|
|
||||||
@include themify($themes) {
|
|
||||||
color: themed("pwLetter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-number {
|
|
||||||
@include themify($themes) {
|
|
||||||
color: themed("pwNumber");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-special {
|
|
||||||
@include themify($themes) {
|
|
||||||
color: themed("pwSpecial");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app-generator {
|
app-generator {
|
||||||
#lengthRange {
|
#lengthRange {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -201,9 +201,6 @@ $themes: (
|
|||||||
navBackgroundAlt: $secondary-alt,
|
navBackgroundAlt: $secondary-alt,
|
||||||
navOrgBackgroundColor: #fbfbfb,
|
navOrgBackgroundColor: #fbfbfb,
|
||||||
navWeight: 600,
|
navWeight: 600,
|
||||||
pwLetter: $body-color,
|
|
||||||
pwNumber: #007fde,
|
|
||||||
pwSpecial: #c40800,
|
|
||||||
pwStrengthBackground: #e9ecef,
|
pwStrengthBackground: #e9ecef,
|
||||||
separator: $secondary,
|
separator: $secondary,
|
||||||
separatorHr: rgb(0, 0, 0, 0.1),
|
separatorHr: rgb(0, 0, 0, 0.1),
|
||||||
@@ -313,9 +310,6 @@ $themes: (
|
|||||||
navBackgroundAlt: $darkDarkBlue1,
|
navBackgroundAlt: $darkDarkBlue1,
|
||||||
navOrgBackgroundColor: #161c26,
|
navOrgBackgroundColor: #161c26,
|
||||||
navWeight: 400,
|
navWeight: 400,
|
||||||
pwLetter: $white,
|
|
||||||
pwNumber: #52bdfb,
|
|
||||||
pwSpecial: #ff7c70,
|
|
||||||
pwStrengthBackground: $darkBlue2,
|
pwStrengthBackground: $darkBlue2,
|
||||||
separator: $darkBlue1,
|
separator: $darkBlue1,
|
||||||
separatorHr: $darkBlue1,
|
separatorHr: $darkBlue1,
|
||||||
|
|||||||
@@ -42,12 +42,10 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
(click)="copyScimUrl()"
|
(click)="copyScimUrl()"
|
||||||
[appA11yTitle]="'copyScimUrl' | i18n"
|
[appA11yTitle]="'copyScimUrl' | i18n"
|
||||||
>
|
></button>
|
||||||
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field *ngIf="showScimSettings">
|
<bit-form-field *ngIf="showScimSettings">
|
||||||
@@ -59,40 +57,33 @@
|
|||||||
id="clientSecret"
|
id="clientSecret"
|
||||||
/>
|
/>
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<button type="button" bitSuffix bitButton (click)="toggleScimKey()">
|
|
||||||
<i
|
|
||||||
aria-hidden="true"
|
|
||||||
class="bwi bwi-lg bwi-eye"
|
|
||||||
[ngClass]="{ 'bwi-eye': !showScimKey, 'bwi-eye-slash': showScimKey }"
|
|
||||||
[appA11yTitle]="'toggleVisibility' | i18n"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container #rotateButton [appApiAction]="rotatePromise">
|
|
||||||
<button
|
<button
|
||||||
[disabled]="$any(rotateButton).loading"
|
|
||||||
type="button"
|
type="button"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
bitButton
|
[disabled]="$any(rotateButton).loading"
|
||||||
|
[bitIconButton]="showScimKey ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
|
(click)="toggleScimKey()"
|
||||||
|
[appA11yTitle]="'toggleVisibility' | i18n"
|
||||||
|
></button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container #rotateButton [appApiAction]="rotatePromise">
|
||||||
|
<!-- TODO: Convert to async actions -->
|
||||||
|
<button
|
||||||
|
[loading]="$any(rotateButton).loading"
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitIconButton="bwi-generate"
|
||||||
(click)="rotateScimKey()"
|
(click)="rotateScimKey()"
|
||||||
[appA11yTitle]="'rotateScimKey' | i18n"
|
[appA11yTitle]="'rotateScimKey' | i18n"
|
||||||
>
|
></button>
|
||||||
<i
|
|
||||||
aria-hidden="true"
|
|
||||||
class="bwi bwi-lg bwi-generate"
|
|
||||||
[ngClass]="{ 'bwi-spin': $any(rotateButton).loading }"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
(click)="copyScimKey()"
|
(click)="copyScimKey()"
|
||||||
[appA11yTitle]="'copyScimKey' | i18n"
|
[appA11yTitle]="'copyScimKey' | i18n"
|
||||||
>
|
></button>
|
||||||
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
|
|
||||||
</button>
|
|
||||||
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
|
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
|
|||||||
@@ -151,28 +151,24 @@
|
|||||||
<bit-label>{{ "callbackPath" | i18n }}</bit-label>
|
<bit-label>{{ "callbackPath" | i18n }}</bit-label>
|
||||||
<input bitInput disabled [value]="callbackPath" />
|
<input bitInput disabled [value]="callbackPath" />
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="callbackPath"
|
[appCopyClick]="callbackPath"
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label>
|
<bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label>
|
||||||
<input bitInput disabled [value]="signedOutCallbackPath" />
|
<input bitInput disabled [value]="signedOutCallbackPath" />
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="signedOutCallbackPath"
|
[appCopyClick]="signedOutCallbackPath"
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
@@ -292,14 +288,12 @@
|
|||||||
<bit-label>{{ "spEntityId" | i18n }}</bit-label>
|
<bit-label>{{ "spEntityId" | i18n }}</bit-label>
|
||||||
<input bitInput disabled [value]="spEntityId" />
|
<input bitInput disabled [value]="spEntityId" />
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="spEntityId"
|
[appCopyClick]="spEntityId"
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
@@ -315,28 +309,24 @@
|
|||||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="spMetadataUrl"
|
[appCopyClick]="spMetadataUrl"
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "spAcsUrl" | i18n }}</bit-label>
|
<bit-label>{{ "spAcsUrl" | i18n }}</bit-label>
|
||||||
<input bitInput disabled [value]="spAcsUrl" />
|
<input bitInput disabled [value]="spAcsUrl" />
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="spAcsUrl"
|
[appCopyClick]="spAcsUrl"
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<bit-icon [icon]="logo" class="tw-w-full tw-text-alt2"></bit-icon>
|
<bit-icon [icon]="logo" class="tw-w-full tw-text-alt2"></bit-icon>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<org-switcher></org-switcher>
|
<org-switcher [filter]="orgFilter"></org-switcher>
|
||||||
<bit-nav-item icon="bwi-collection" text="Projects" route="projects"></bit-nav-item>
|
<bit-nav-item icon="bwi-collection" text="Projects" route="projects"></bit-nav-item>
|
||||||
<bit-nav-item icon="bwi-key" text="Secrets" route="secrets"></bit-nav-item>
|
<bit-nav-item icon="bwi-key" text="Secrets" route="secrets"></bit-nav-item>
|
||||||
<bit-nav-item icon="bwi-wrench" text="Service Accounts" route="service-accounts"></bit-nav-item>
|
<bit-nav-item icon="bwi-wrench" text="Service Accounts" route="service-accounts"></bit-nav-item>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
|
||||||
import { SecretsManagerLogo } from "./secrets-manager-logo";
|
import { SecretsManagerLogo } from "./secrets-manager-logo";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -8,4 +10,6 @@ import { SecretsManagerLogo } from "./secrets-manager-logo";
|
|||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class NavigationComponent {
|
||||||
protected readonly logo = SecretsManagerLogo;
|
protected readonly logo = SecretsManagerLogo;
|
||||||
|
|
||||||
|
protected orgFilter = (org: Organization) => org.canAccessSecretsManager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,22 @@ import type { Organization } from "@bitwarden/common/models/domain/organization"
|
|||||||
export class OrgSwitcherComponent {
|
export class OrgSwitcherComponent {
|
||||||
protected organizations$: Observable<Organization[]> =
|
protected organizations$: Observable<Organization[]> =
|
||||||
this.organizationService.organizations$.pipe(
|
this.organizationService.organizations$.pipe(
|
||||||
map((orgs) => orgs.sort((a, b) => a.name.localeCompare(b.name)))
|
map((orgs) => orgs.filter(this.filter).sort((a, b) => a.name.localeCompare(b.name)))
|
||||||
);
|
);
|
||||||
protected activeOrganization$: Observable<Organization> = combineLatest([
|
protected activeOrganization$: Observable<Organization> = combineLatest([
|
||||||
this.route.paramMap,
|
this.route.paramMap,
|
||||||
this.organizationService.organizations$,
|
this.organizations$,
|
||||||
]).pipe(map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId"))));
|
]).pipe(map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId"))));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter function for displayed organizations in the `org-switcher`
|
||||||
|
* @example
|
||||||
|
* const smFilter = (org: Organization) => org.canAccessSecretsManager
|
||||||
|
* // <org-switcher [filter]="smFilter">
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
filter: (org: Organization) => boolean = () => true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is `true` if the expanded content is visible
|
* Is `true` if the expanded content is visible
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
formPromise: Promise<string>;
|
formPromise: Promise<string>;
|
||||||
disabledByPolicy = false;
|
disabledByPolicy = false;
|
||||||
showFilePassword: boolean;
|
|
||||||
showConfirmFilePassword: boolean;
|
|
||||||
|
|
||||||
exportForm = this.formBuilder.group({
|
exportForm = this.formBuilder.group({
|
||||||
format: ["json"],
|
format: ["json"],
|
||||||
@@ -199,16 +197,6 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
return this.exportForm.get("fileEncryptionType").value;
|
return this.exportForm.get("fileEncryptionType").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFilePassword() {
|
|
||||||
this.showFilePassword = !this.showFilePassword;
|
|
||||||
document.getElementById("filePassword").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleConfirmFilePassword() {
|
|
||||||
this.showConfirmFilePassword = !this.showConfirmFilePassword;
|
|
||||||
document.getElementById("confirmFilePassword").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustValidators() {
|
adjustValidators() {
|
||||||
this.exportForm.get("confirmFilePassword").reset();
|
this.exportForm.get("confirmFilePassword").reset();
|
||||||
this.exportForm.get("filePassword").reset();
|
this.exportForm.get("filePassword").reset();
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import { SelectCopyDirective } from "./directives/select-copy.directive";
|
|||||||
import { StopClickDirective } from "./directives/stop-click.directive";
|
import { StopClickDirective } from "./directives/stop-click.directive";
|
||||||
import { StopPropDirective } from "./directives/stop-prop.directive";
|
import { StopPropDirective } from "./directives/stop-prop.directive";
|
||||||
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
||||||
import { ColorPasswordCountPipe } from "./pipes/color-password-count.pipe";
|
|
||||||
import { ColorPasswordPipe } from "./pipes/color-password.pipe";
|
|
||||||
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
|
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
|
||||||
import { EllipsisPipe } from "./pipes/ellipsis.pipe";
|
import { EllipsisPipe } from "./pipes/ellipsis.pipe";
|
||||||
import { I18nPipe } from "./pipes/i18n.pipe";
|
import { I18nPipe } from "./pipes/i18n.pipe";
|
||||||
@@ -50,8 +48,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
|||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
CalloutComponent,
|
CalloutComponent,
|
||||||
ColorPasswordCountPipe,
|
|
||||||
ColorPasswordPipe,
|
|
||||||
CreditCardNumberPipe,
|
CreditCardNumberPipe,
|
||||||
EllipsisPipe,
|
EllipsisPipe,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
@@ -81,8 +77,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
|||||||
BitwardenToastModule,
|
BitwardenToastModule,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
CalloutComponent,
|
CalloutComponent,
|
||||||
ColorPasswordCountPipe,
|
|
||||||
ColorPasswordPipe,
|
|
||||||
CreditCardNumberPipe,
|
CreditCardNumberPipe,
|
||||||
EllipsisPipe,
|
EllipsisPipe,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
|
|||||||
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer";
|
||||||
|
|
||||||
|
import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json";
|
||||||
|
import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json";
|
||||||
|
|
||||||
|
describe("Passky Json Importer", () => {
|
||||||
|
let importer: Importer;
|
||||||
|
beforeEach(() => {
|
||||||
|
importer = new Importer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not import encrypted backups", async () => {
|
||||||
|
const testDataJson = JSON.stringify(EncryptedData);
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.errorMessage).toBe("Unable to import an encrypted passky backup.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse login data", async () => {
|
||||||
|
const testDataJson = JSON.stringify(UnencryptedData);
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const cipher = result.ciphers.shift();
|
||||||
|
expect(cipher.name).toEqual("https://bitwarden.com/");
|
||||||
|
expect(cipher.login.username).toEqual("testUser");
|
||||||
|
expect(cipher.login.password).toEqual("testPassword");
|
||||||
|
expect(cipher.login.uris.length).toEqual(1);
|
||||||
|
const uriView = cipher.login.uris.shift();
|
||||||
|
expect(uriView.uri).toEqual("https://bitwarden.com/");
|
||||||
|
expect(cipher.notes).toEqual("my notes");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||||
|
|
||||||
|
export const testData: PasskyJsonExport = {
|
||||||
|
encrypted: true,
|
||||||
|
passwords: [
|
||||||
|
{
|
||||||
|
website:
|
||||||
|
"w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp",
|
||||||
|
username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==",
|
||||||
|
password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+",
|
||||||
|
message:
|
||||||
|
"w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||||
|
|
||||||
|
export const testData: PasskyJsonExport = {
|
||||||
|
encrypted: false,
|
||||||
|
passwords: [
|
||||||
|
{
|
||||||
|
website: "https://bitwarden.com/",
|
||||||
|
username: "testUser",
|
||||||
|
password: "testPassword",
|
||||||
|
message: "my notes",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@ export const regularImportOptions = [
|
|||||||
{ id: "encryptrcsv", name: "Encryptr (csv)" },
|
{ id: "encryptrcsv", name: "Encryptr (csv)" },
|
||||||
{ id: "yoticsv", name: "Yoti (csv)" },
|
{ id: "yoticsv", name: "Yoti (csv)" },
|
||||||
{ id: "nordpasscsv", name: "Nordpass (csv)" },
|
{ id: "nordpasscsv", name: "Nordpass (csv)" },
|
||||||
|
{ id: "passkyjson", name: "Passky (json)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ImportType =
|
export type ImportType =
|
||||||
|
|||||||
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ImportResult } from "../../models/domain/import-result";
|
||||||
|
import { BaseImporter } from "../base-importer";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { PasskyJsonExport } from "./passky-json-types";
|
||||||
|
|
||||||
|
export class PasskyJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const passkyExport: PasskyJsonExport = JSON.parse(data);
|
||||||
|
if (
|
||||||
|
passkyExport == null ||
|
||||||
|
passkyExport.passwords == null ||
|
||||||
|
passkyExport.passwords.length === 0
|
||||||
|
) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passkyExport.encrypted == true) {
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessage = "Unable to import an encrypted passky backup.";
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
passkyExport.passwords.forEach((record) => {
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = record.website;
|
||||||
|
cipher.login.username = record.username;
|
||||||
|
cipher.login.password = record.password;
|
||||||
|
|
||||||
|
cipher.login.uris = this.makeUriArray(record.website);
|
||||||
|
cipher.notes = record.message;
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface PasskyJsonExport {
|
||||||
|
encrypted: boolean;
|
||||||
|
passwords: LoginEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginEntry {
|
||||||
|
website: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ import { OnePasswordMacCsvImporter } from "../importers/onepassword/onepassword-
|
|||||||
import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer";
|
import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer";
|
||||||
import { PadlockCsvImporter } from "../importers/padlock-csv-importer";
|
import { PadlockCsvImporter } from "../importers/padlock-csv-importer";
|
||||||
import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer";
|
import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer";
|
||||||
|
import { PasskyJsonImporter } from "../importers/passky/passky-json-importer";
|
||||||
import { PassmanJsonImporter } from "../importers/passman-json-importer";
|
import { PassmanJsonImporter } from "../importers/passman-json-importer";
|
||||||
import { PasspackCsvImporter } from "../importers/passpack-csv-importer";
|
import { PasspackCsvImporter } from "../importers/passpack-csv-importer";
|
||||||
import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer";
|
import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer";
|
||||||
@@ -279,6 +280,8 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
return new YotiCsvImporter();
|
return new YotiCsvImporter();
|
||||||
case "nordpasscsv":
|
case "nordpasscsv":
|
||||||
return new NordPassCsvImporter();
|
return new NordPassCsvImporter();
|
||||||
|
case "passkyjson":
|
||||||
|
return new PasskyJsonImporter();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const template = `
|
|||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Email</bit-label>
|
<bit-label>Email</bit-label>
|
||||||
<input bitInput formControlName="email" />
|
<input bitInput formControlName="email" />
|
||||||
|
<button type="button" bitSuffix bitIconButton="bwi-refresh" bitFormButton [bitAction]="refresh"></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
|
<button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
|
||||||
@@ -47,6 +48,12 @@ class PromiseExampleComponent {
|
|||||||
|
|
||||||
constructor(private formBuilder: FormBuilder) {}
|
constructor(private formBuilder: FormBuilder) {}
|
||||||
|
|
||||||
|
refresh = async () => {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
setTimeout(resolve, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
this.formObj.markAllAsTouched();
|
this.formObj.markAllAsTouched();
|
||||||
|
|
||||||
@@ -78,6 +85,10 @@ class ObservableExampleComponent {
|
|||||||
|
|
||||||
constructor(private formBuilder: FormBuilder) {}
|
constructor(private formBuilder: FormBuilder) {}
|
||||||
|
|
||||||
|
refresh = () => {
|
||||||
|
return of("fake observable").pipe(delay(2000));
|
||||||
|
};
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
this.formObj.markAllAsTouched();
|
this.formObj.markAllAsTouched();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<span class="tw-relative">
|
<span class="tw-relative">
|
||||||
<span [ngClass]="{ 'tw-invisible': loading }">
|
<span [ngClass]="{ 'tw-invisible': loading }">
|
||||||
<i class="bwi bwi-lg" [ngClass]="iconClass" aria-hidden="true" *ngIf="icon"></i>
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ describe("Button", () => {
|
|||||||
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
||||||
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
||||||
|
|
||||||
|
testAppComponent.buttonType = "unstyled";
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(
|
||||||
|
Array.from(buttonDebugElement.nativeElement.classList).some((klass: string) =>
|
||||||
|
klass.startsWith("tw-bg")
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
Array.from(linkDebugElement.nativeElement.classList).some((klass: string) =>
|
||||||
|
klass.startsWith("tw-bg")
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
testAppComponent.buttonType = null;
|
testAppComponent.buttonType = null;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
|
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Input, HostBinding, Component } from "@angular/core";
|
import { Input, HostBinding, Component } from "@angular/core";
|
||||||
|
|
||||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||||
|
|
||||||
export type ButtonTypes = "primary" | "secondary" | "danger";
|
const focusRing = [
|
||||||
|
"focus-visible:tw-ring",
|
||||||
|
"focus-visible:tw-ring-offset-2",
|
||||||
|
"focus-visible:tw-ring-primary-700",
|
||||||
|
"focus-visible:tw-z-10",
|
||||||
|
];
|
||||||
|
|
||||||
const buttonStyles: Record<ButtonTypes, string[]> = {
|
const buttonStyles: Record<ButtonType, string[]> = {
|
||||||
primary: [
|
primary: [
|
||||||
"tw-border-primary-500",
|
"tw-border-primary-500",
|
||||||
"tw-bg-primary-500",
|
"tw-bg-primary-500",
|
||||||
@@ -15,6 +20,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
|||||||
"disabled:tw-border-primary-500/60",
|
"disabled:tw-border-primary-500/60",
|
||||||
"disabled:!tw-text-contrast/60",
|
"disabled:!tw-text-contrast/60",
|
||||||
"disabled:tw-bg-clip-padding",
|
"disabled:tw-bg-clip-padding",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
secondary: [
|
secondary: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -26,6 +32,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
|||||||
"disabled:tw-bg-transparent",
|
"disabled:tw-bg-transparent",
|
||||||
"disabled:tw-border-text-muted/60",
|
"disabled:tw-border-text-muted/60",
|
||||||
"disabled:!tw-text-muted/60",
|
"disabled:!tw-text-muted/60",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
danger: [
|
danger: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -37,7 +44,9 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
|||||||
"disabled:tw-bg-transparent",
|
"disabled:tw-bg-transparent",
|
||||||
"disabled:tw-border-danger-500/60",
|
"disabled:tw-border-danger-500/60",
|
||||||
"disabled:!tw-text-danger/60",
|
"disabled:!tw-text-danger/60",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
|
unstyled: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,10 +67,6 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
"tw-text-center",
|
"tw-text-center",
|
||||||
"hover:tw-no-underline",
|
"hover:tw-no-underline",
|
||||||
"focus:tw-outline-none",
|
"focus:tw-outline-none",
|
||||||
"focus-visible:tw-ring",
|
|
||||||
"focus-visible:tw-ring-offset-2",
|
|
||||||
"focus-visible:tw-ring-primary-700",
|
|
||||||
"focus-visible:tw-z-10",
|
|
||||||
]
|
]
|
||||||
.concat(
|
.concat(
|
||||||
this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"]
|
this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"]
|
||||||
@@ -75,17 +80,14 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
return disabled || this.loading ? true : null;
|
return disabled || this.loading ? true : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() buttonType: ButtonTypes = null;
|
@Input() buttonType: ButtonType;
|
||||||
|
|
||||||
@Input() block?: boolean;
|
@Input() block?: boolean;
|
||||||
|
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
|
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
|
|
||||||
@Input("bitIconButton") icon: string;
|
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||||
|
this.buttonType = value;
|
||||||
get iconClass() {
|
|
||||||
return [this.icon, "!tw-m-0"];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,17 +101,3 @@ export const Block = BlockTemplate.bind({});
|
|||||||
Block.args = {
|
Block.args = {
|
||||||
block: true,
|
block: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const IconTemplate: Story = (args) => ({
|
|
||||||
props: args,
|
|
||||||
template: `
|
|
||||||
<button bitButton [bitIconButton]="icon" buttonType="primary" class="tw-mr-2"></button>
|
|
||||||
<button bitButton [bitIconButton]="icon"buttonType="secondary" class="tw-mr-2"></button>
|
|
||||||
<button bitButton [bitIconButton]="icon" buttonType="danger" class="tw-mr-2"></button>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Icon = IconTemplate.bind({});
|
|
||||||
Icon.args = {
|
|
||||||
icon: "bwi-eye",
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ export class ColorPasswordComponent {
|
|||||||
|
|
||||||
if (this.showCount) {
|
if (this.showCount) {
|
||||||
return charClass.concat([
|
return charClass.concat([
|
||||||
"tw-inline-flex",
|
|
||||||
"tw-flex-col",
|
"tw-flex-col",
|
||||||
"tw-items-center",
|
"tw-items-center",
|
||||||
"tw-w-7",
|
"tw-w-7",
|
||||||
"tw-py-1",
|
"tw-py-1",
|
||||||
"odd:tw-bg-secondary-100",
|
"odd:tw-bg-secondary-100",
|
||||||
|
"even:tw-bg-background",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
|||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { AsyncActionsModule } from "../async-actions";
|
||||||
import { ButtonModule } from "../button";
|
import { ButtonModule } from "../button";
|
||||||
import { CheckboxModule } from "../checkbox";
|
import { CheckboxModule } from "../checkbox";
|
||||||
|
import { IconButtonModule } from "../icon-button";
|
||||||
import { InputModule } from "../input/input.module";
|
import { InputModule } from "../input/input.module";
|
||||||
import { RadioButtonModule } from "../radio-button";
|
import { RadioButtonModule } from "../radio-button";
|
||||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
@@ -31,6 +33,8 @@ export default {
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
InputModule,
|
InputModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
AsyncActionsModule,
|
||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
RadioButtonModule,
|
RadioButtonModule,
|
||||||
],
|
],
|
||||||
@@ -177,10 +181,13 @@ const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldCom
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Label</bit-label>
|
<button bitPrefix bitIconButton="bwi-star"></button>
|
||||||
<input bitInput placeholder="Placeholder" type="password" />
|
<input bitInput placeholder="Placeholder" />
|
||||||
<button bitSuffix bitButton bitIconButton="bwi-eye"></button>
|
<button bitSuffix bitIconButton="bwi-eye"></button>
|
||||||
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
|
<button bitSuffix bitIconButton="bwi-clone"></button>
|
||||||
|
<button bitSuffix bitButton>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
@@ -195,9 +202,13 @@ const DisabledButtonInputGroupTemplate: Story<BitFormFieldComponent> = (
|
|||||||
template: `
|
template: `
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Label</bit-label>
|
<bit-label>Label</bit-label>
|
||||||
|
<button bitPrefix bitIconButton="bwi-star" disabled></button>
|
||||||
<input bitInput placeholder="Placeholder" disabled />
|
<input bitInput placeholder="Placeholder" disabled />
|
||||||
<button bitSuffix bitButton bitIconButton="bwi-eye" disabled></button>
|
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
|
||||||
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
|
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
|
||||||
|
<button bitSuffix bitButton disabled>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import {
|
|||||||
Directive,
|
Directive,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Host,
|
Host,
|
||||||
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
Output,
|
Output,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { ButtonComponent } from "../button";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||||
|
|
||||||
import { BitFormFieldComponent } from "./form-field.component";
|
import { BitFormFieldComponent } from "./form-field.component";
|
||||||
|
|
||||||
@@ -17,9 +20,18 @@ import { BitFormFieldComponent } from "./form-field.component";
|
|||||||
selector: "[bitPasswordInputToggle]",
|
selector: "[bitPasswordInputToggle]",
|
||||||
})
|
})
|
||||||
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
||||||
@Input() toggled = false;
|
/**
|
||||||
|
* Whether the input is toggled to show the password.
|
||||||
|
*/
|
||||||
|
@HostBinding("attr.aria-pressed") @Input() toggled = false;
|
||||||
@Output() toggledChange = new EventEmitter<boolean>();
|
@Output() toggledChange = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
|
||||||
|
@HostBinding("attr.aria-label") label = this.i18nService.t("toggleVisibility");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler to toggle the state of the input type.
|
||||||
|
*/
|
||||||
@HostListener("click") onClick() {
|
@HostListener("click") onClick() {
|
||||||
this.toggled = !this.toggled;
|
this.toggled = !this.toggled;
|
||||||
this.toggledChange.emit(this.toggled);
|
this.toggledChange.emit(this.toggled);
|
||||||
@@ -29,7 +41,11 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
|||||||
this.formField.input?.focus();
|
this.formField.input?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(@Host() private button: ButtonComponent, private formField: BitFormFieldComponent) {}
|
constructor(
|
||||||
|
@Host() private button: BitIconButtonComponent,
|
||||||
|
private formField: BitFormFieldComponent,
|
||||||
|
private i18nService: I18nService
|
||||||
|
) {}
|
||||||
|
|
||||||
get icon() {
|
get icon() {
|
||||||
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
|
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ import { Component, DebugElement } from "@angular/core";
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
|
|
||||||
import { ButtonComponent, ButtonModule } from "../button";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { IconButtonModule } from "../icon-button";
|
||||||
|
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||||
import { InputModule } from "../input/input.module";
|
import { InputModule } from "../input/input.module";
|
||||||
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
import { BitFormFieldControl } from "./form-field-control";
|
import { BitFormFieldControl } from "./form-field-control";
|
||||||
import { BitFormFieldComponent } from "./form-field.component";
|
import { BitFormFieldComponent } from "./form-field.component";
|
||||||
@@ -17,7 +21,7 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi
|
|||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Password</bit-label>
|
<bit-label>Password</bit-label>
|
||||||
<input bitInput type="password" />
|
<input bitInput type="password" />
|
||||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
@@ -26,21 +30,22 @@ class TestFormFieldComponent {}
|
|||||||
|
|
||||||
describe("PasswordInputToggle", () => {
|
describe("PasswordInputToggle", () => {
|
||||||
let fixture: ComponentFixture<TestFormFieldComponent>;
|
let fixture: ComponentFixture<TestFormFieldComponent>;
|
||||||
let button: ButtonComponent;
|
let button: BitIconButtonComponent;
|
||||||
let input: BitFormFieldControl;
|
let input: BitFormFieldControl;
|
||||||
let toggle: DebugElement;
|
let toggle: DebugElement;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FormFieldModule, ButtonModule, InputModule],
|
imports: [FormFieldModule, IconButtonModule, InputModule],
|
||||||
declarations: [TestFormFieldComponent],
|
declarations: [TestFormFieldComponent],
|
||||||
|
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(TestFormFieldComponent);
|
fixture = TestBed.createComponent(TestFormFieldComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective));
|
toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective));
|
||||||
const buttonEl = fixture.debugElement.query(By.directive(ButtonComponent));
|
const buttonEl = fixture.debugElement.query(By.directive(BitIconButtonComponent));
|
||||||
button = buttonEl.componentInstance;
|
button = buttonEl.componentInstance;
|
||||||
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
|
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
|
||||||
const formField: BitFormFieldComponent = formFieldEl.componentInstance;
|
const formField: BitFormFieldComponent = formFieldEl.componentInstance;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||||
|
|
||||||
import { ButtonModule } from "../button";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { IconButtonModule } from "../icon-button";
|
||||||
import { InputModule } from "../input/input.module";
|
import { InputModule } from "../input/input.module";
|
||||||
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
import { FormFieldModule } from "./form-field.module";
|
import { FormFieldModule } from "./form-field.module";
|
||||||
import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive";
|
import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive";
|
||||||
@@ -12,7 +15,13 @@ export default {
|
|||||||
component: BitPasswordInputToggleDirective,
|
component: BitPasswordInputToggleDirective,
|
||||||
decorators: [
|
decorators: [
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
|
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, IconButtonModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }),
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -40,7 +49,7 @@ const Template: Story<BitPasswordInputToggleDirective> = (
|
|||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Password</bit-label>
|
<bit-label>Password</bit-label>
|
||||||
<input bitInput type="password" />
|
<input bitInput type="password" />
|
||||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
@@ -60,7 +69,7 @@ const TemplateBinding: Story<BitPasswordInputToggleDirective> = (
|
|||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>Password</bit-label>
|
<bit-label>Password</bit-label>
|
||||||
<input bitInput type="password" />
|
<input bitInput type="password" />
|
||||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<label class="tw-text-main">
|
<label class="tw-text-main">
|
||||||
|
|||||||
@@ -1,24 +1,51 @@
|
|||||||
import { Directive, HostBinding, Input } from "@angular/core";
|
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||||
|
|
||||||
|
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||||
|
|
||||||
export const PrefixClasses = [
|
export const PrefixClasses = [
|
||||||
"tw-block",
|
|
||||||
"tw-px-3",
|
|
||||||
"tw-py-1.5",
|
|
||||||
"tw-bg-background-alt",
|
"tw-bg-background-alt",
|
||||||
"tw-border",
|
"tw-border",
|
||||||
"tw-border-solid",
|
"tw-border-solid",
|
||||||
"tw-border-secondary-500",
|
"tw-border-secondary-500",
|
||||||
"tw-text-muted",
|
"tw-text-muted",
|
||||||
"tw-rounded-none",
|
"tw-rounded-none",
|
||||||
"disabled:!tw-text-muted",
|
|
||||||
"disabled:tw-border-secondary-500",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PrefixButtonClasses = [
|
||||||
|
"hover:tw-bg-text-muted",
|
||||||
|
"hover:tw-text-contrast",
|
||||||
|
"disabled:tw-opacity-100",
|
||||||
|
"disabled:tw-bg-secondary-100",
|
||||||
|
"disabled:hover:tw-bg-secondary-100",
|
||||||
|
"disabled:hover:tw-text-muted",
|
||||||
|
"focus-visible:tw-ring-primary-700",
|
||||||
|
|
||||||
|
"focus-visible:tw-border-primary-700",
|
||||||
|
"focus-visible:tw-ring-1",
|
||||||
|
"focus-visible:tw-ring-inset",
|
||||||
|
"focus-visible:tw-ring-primary-700",
|
||||||
|
"focus-visible:tw-z-10",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PrefixStaticContentClasses = ["tw-block", "tw-px-3", "tw-py-1.5"];
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitPrefix]",
|
selector: "[bitPrefix]",
|
||||||
})
|
})
|
||||||
export class BitPrefixDirective {
|
export class BitPrefixDirective implements OnInit {
|
||||||
|
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
|
||||||
|
|
||||||
@HostBinding("class") @Input() get classList() {
|
@HostBinding("class") @Input() get classList() {
|
||||||
return PrefixClasses.concat(["tw-border-r-0", "first:tw-rounded-l"]);
|
return PrefixClasses.concat([
|
||||||
|
"tw-border-r-0",
|
||||||
|
"first:tw-rounded-l",
|
||||||
|
|
||||||
|
"focus-visible:tw-border-r",
|
||||||
|
"focus-visible:tw-mr-[-1px]",
|
||||||
|
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.buttonComponent?.setButtonType("unstyled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import { Directive, HostBinding, Input } from "@angular/core";
|
import { Directive, HostBinding, Input, Optional } from "@angular/core";
|
||||||
|
|
||||||
import { PrefixClasses } from "./prefix.directive";
|
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||||
|
|
||||||
|
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitSuffix]",
|
selector: "[bitSuffix]",
|
||||||
})
|
})
|
||||||
export class BitSuffixDirective {
|
export class BitSuffixDirective {
|
||||||
|
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
|
||||||
|
|
||||||
@HostBinding("class") @Input() get classList() {
|
@HostBinding("class") @Input() get classList() {
|
||||||
return PrefixClasses.concat(["tw-border-l-0", "last:tw-rounded-r"]);
|
return PrefixClasses.concat([
|
||||||
|
"tw-border-l-0",
|
||||||
|
"last:tw-rounded-r",
|
||||||
|
|
||||||
|
"focus-visible:tw-border-l",
|
||||||
|
"focus-visible:tw-ml-[-1px]",
|
||||||
|
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.buttonComponent?.setButtonType("unstyled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import { Component, HostBinding, Input } from "@angular/core";
|
import { Component, HostBinding, Input } from "@angular/core";
|
||||||
|
|
||||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||||
|
|
||||||
export type IconButtonType = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
|
export type IconButtonType = ButtonType | "contrast" | "main" | "muted";
|
||||||
|
|
||||||
|
const focusRing = [
|
||||||
|
// Workaround for box-shadow with transparent offset issue:
|
||||||
|
// https://github.com/tailwindlabs/tailwindcss/issues/3595
|
||||||
|
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
|
||||||
|
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
|
||||||
|
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
|
||||||
|
"tw-relative",
|
||||||
|
"before:tw-content-['']",
|
||||||
|
"before:tw-block",
|
||||||
|
"before:tw-absolute",
|
||||||
|
"before:-tw-inset-[3px]",
|
||||||
|
"before:tw-rounded-md",
|
||||||
|
"before:tw-transition",
|
||||||
|
"before:tw-ring",
|
||||||
|
"before:tw-ring-transparent",
|
||||||
|
"focus-visible:tw-z-10",
|
||||||
|
];
|
||||||
|
|
||||||
const styles: Record<IconButtonType, string[]> = {
|
const styles: Record<IconButtonType, string[]> = {
|
||||||
contrast: [
|
contrast: [
|
||||||
@@ -12,8 +30,10 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:tw-bg-transparent-hover",
|
"hover:tw-bg-transparent-hover",
|
||||||
"hover:tw-border-text-contrast",
|
"hover:tw-border-text-contrast",
|
||||||
"focus-visible:before:tw-ring-text-contrast",
|
"focus-visible:before:tw-ring-text-contrast",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-transparent",
|
"disabled:hover:tw-border-transparent",
|
||||||
"disabled:hover:tw-bg-transparent",
|
"disabled:hover:tw-bg-transparent",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
main: [
|
main: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -22,8 +42,10 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:tw-bg-transparent-hover",
|
"hover:tw-bg-transparent-hover",
|
||||||
"hover:tw-border-text-main",
|
"hover:tw-border-text-main",
|
||||||
"focus-visible:before:tw-ring-text-main",
|
"focus-visible:before:tw-ring-text-main",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-transparent",
|
"disabled:hover:tw-border-transparent",
|
||||||
"disabled:hover:tw-bg-transparent",
|
"disabled:hover:tw-bg-transparent",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
muted: [
|
muted: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -32,8 +54,10 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:tw-bg-transparent-hover",
|
"hover:tw-bg-transparent-hover",
|
||||||
"hover:tw-border-primary-700",
|
"hover:tw-border-primary-700",
|
||||||
"focus-visible:before:tw-ring-primary-700",
|
"focus-visible:before:tw-ring-primary-700",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-transparent",
|
"disabled:hover:tw-border-transparent",
|
||||||
"disabled:hover:tw-bg-transparent",
|
"disabled:hover:tw-bg-transparent",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
primary: [
|
primary: [
|
||||||
"tw-bg-primary-500",
|
"tw-bg-primary-500",
|
||||||
@@ -42,8 +66,10 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:tw-bg-primary-700",
|
"hover:tw-bg-primary-700",
|
||||||
"hover:tw-border-primary-700",
|
"hover:tw-border-primary-700",
|
||||||
"focus-visible:before:tw-ring-primary-700",
|
"focus-visible:before:tw-ring-primary-700",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-primary-500",
|
"disabled:hover:tw-border-primary-500",
|
||||||
"disabled:hover:tw-bg-primary-500",
|
"disabled:hover:tw-bg-primary-500",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
secondary: [
|
secondary: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -52,10 +78,12 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:!tw-text-contrast",
|
"hover:!tw-text-contrast",
|
||||||
"hover:tw-bg-text-muted",
|
"hover:tw-bg-text-muted",
|
||||||
"focus-visible:before:tw-ring-primary-700",
|
"focus-visible:before:tw-ring-primary-700",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-text-muted",
|
"disabled:hover:tw-border-text-muted",
|
||||||
"disabled:hover:tw-bg-transparent",
|
"disabled:hover:tw-bg-transparent",
|
||||||
"disabled:hover:!tw-text-muted",
|
"disabled:hover:!tw-text-muted",
|
||||||
"disabled:hover:tw-border-text-muted",
|
"disabled:hover:tw-border-text-muted",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
danger: [
|
danger: [
|
||||||
"tw-bg-transparent",
|
"tw-bg-transparent",
|
||||||
@@ -64,11 +92,14 @@ const styles: Record<IconButtonType, string[]> = {
|
|||||||
"hover:!tw-text-contrast",
|
"hover:!tw-text-contrast",
|
||||||
"hover:tw-bg-danger-500",
|
"hover:tw-bg-danger-500",
|
||||||
"focus-visible:before:tw-ring-primary-700",
|
"focus-visible:before:tw-ring-primary-700",
|
||||||
|
"disabled:tw-opacity-60",
|
||||||
"disabled:hover:tw-border-danger-500",
|
"disabled:hover:tw-border-danger-500",
|
||||||
"disabled:hover:tw-bg-transparent",
|
"disabled:hover:tw-bg-transparent",
|
||||||
"disabled:hover:!tw-text-danger",
|
"disabled:hover:!tw-text-danger",
|
||||||
"disabled:hover:tw-border-danger-500",
|
"disabled:hover:tw-border-danger-500",
|
||||||
|
...focusRing,
|
||||||
],
|
],
|
||||||
|
unstyled: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconButtonSize = "default" | "small";
|
export type IconButtonSize = "default" | "small";
|
||||||
@@ -86,7 +117,7 @@ const sizes: Record<IconButtonSize, string[]> = {
|
|||||||
export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
||||||
@Input("bitIconButton") icon: string;
|
@Input("bitIconButton") icon: string;
|
||||||
|
|
||||||
@Input() buttonType: IconButtonType = "main";
|
@Input() buttonType: IconButtonType;
|
||||||
|
|
||||||
@Input() size: IconButtonSize = "default";
|
@Input() size: IconButtonSize = "default";
|
||||||
|
|
||||||
@@ -98,27 +129,9 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
|||||||
"tw-rounded",
|
"tw-rounded",
|
||||||
"tw-transition",
|
"tw-transition",
|
||||||
"hover:tw-no-underline",
|
"hover:tw-no-underline",
|
||||||
"disabled:tw-opacity-60",
|
|
||||||
"focus:tw-outline-none",
|
"focus:tw-outline-none",
|
||||||
|
|
||||||
// Workaround for box-shadow with transparent offset issue:
|
|
||||||
// https://github.com/tailwindlabs/tailwindcss/issues/3595
|
|
||||||
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
|
|
||||||
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
|
|
||||||
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
|
|
||||||
"tw-relative",
|
|
||||||
"before:tw-content-['']",
|
|
||||||
"before:tw-block",
|
|
||||||
"before:tw-absolute",
|
|
||||||
"before:-tw-inset-[3px]",
|
|
||||||
"before:tw-rounded-md",
|
|
||||||
"before:tw-transition",
|
|
||||||
"before:tw-ring",
|
|
||||||
"before:tw-ring-transparent",
|
|
||||||
"focus-visible:before:tw-ring-text-contrast",
|
|
||||||
"focus-visible:tw-z-10",
|
|
||||||
]
|
]
|
||||||
.concat(styles[this.buttonType])
|
.concat(styles[this.buttonType ?? "main"])
|
||||||
.concat(sizes[this.size]);
|
.concat(sizes[this.size]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +147,8 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
|||||||
|
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
|
|
||||||
|
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||||
|
this.buttonType = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
|||||||
"focus:tw-outline-none",
|
"focus:tw-outline-none",
|
||||||
"focus:tw-border-primary-700",
|
"focus:tw-border-primary-700",
|
||||||
"focus:tw-ring-1",
|
"focus:tw-ring-1",
|
||||||
|
"focus:tw-ring-inset",
|
||||||
"focus:tw-ring-primary-700",
|
"focus:tw-ring-primary-700",
|
||||||
"focus:tw-z-10",
|
"focus:tw-z-10",
|
||||||
"disabled:tw-bg-secondary-100",
|
"disabled:tw-bg-secondary-100",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
|
||||||
|
|
||||||
export abstract class ButtonLikeAbstraction {
|
export abstract class ButtonLikeAbstraction {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
setButtonType: (value: ButtonType) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -177,7 +177,7 @@
|
|||||||
},
|
},
|
||||||
"apps/browser": {
|
"apps/browser": {
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2022.12.0"
|
"version": "2022.12.1"
|
||||||
},
|
},
|
||||||
"apps/cli": {
|
"apps/cli": {
|
||||||
"name": "@bitwarden/cli",
|
"name": "@bitwarden/cli",
|
||||||
|
|||||||
Reference in New Issue
Block a user