1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
Alec Rippberger
2024-10-15 11:22:24 -05:00
committed by GitHub
277 changed files with 12591 additions and 1445 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View File

@@ -239,11 +239,15 @@ $icons: (
"github": "\e950",
"facebook": "\e94d",
"paypal": "\e938",
"google": "\e951",
"brave": "\e951",
"google": "\e9a5",
"duckduckgo": "\e9bb",
"tor": "\e9bc",
"vivaldi": "\e9bd",
"linkedin": "\e955",
"discourse": "\e91e",
"twitter": "\e961",
"x-twitter": "\e9a5",
"x-twitter": "\e9be",
"youtube": "\e966",
"windows": "\e964",
"apple": "\e945",
@@ -276,6 +280,21 @@ $icons: (
"popout": "\e9aa",
"wand": "\e9a6",
"msp": "\e9a1",
"totp-codes-alt": "\e9ac",
"totp-codes-alt2": "\e9ad",
"totp-codes": "\e9ae",
"authenticator": "\e9af",
"fingerprint": "\e9b0",
"expired": "\e9ba",
"icon-1": "\e9b1",
"icon-2": "\e9b2",
"icon-3": "\e9b3",
"icon-4": "\e9b4",
"icon-5": "\e9b5",
"icon-6": "\e9b6",
"icon-7": "\e9b7",
"icon-8": "\e9b8",
"icon-9": "\e9b9",
);
@each $name, $glyph in $icons {

View File

@@ -4,20 +4,34 @@ import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
import { AnonLayoutComponent } from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Icon } from "@bitwarden/components";
import { Icon, Translation } from "@bitwarden/components";
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
export interface AnonLayoutWrapperData {
pageTitle?: string;
pageSubtitle?:
| string
| {
subtitle: string;
translate: boolean;
};
/**
* The optional title of the page.
* If a string is provided, it will be presented as is (ex: Organization name)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageTitle?: string | Translation;
/**
* The optional subtitle of the page.
* If a string is provided, it will be presented as is (ex: user's email)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageSubtitle?: string | Translation;
/**
* The optional icon to display on the page.
*/
pageIcon?: Icon;
/**
* Optional flag to either show the optional environment selector (false) or just a readonly hostname (true).
*/
showReadonlyHostname?: boolean;
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: "md" | "3xl";
}
@@ -71,11 +85,11 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
}
if (firstChildRouteData["pageTitle"] !== undefined) {
this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]);
this.pageTitle = this.handleStringOrTranslation(firstChildRouteData["pageTitle"]);
}
if (firstChildRouteData["pageSubtitle"] !== undefined) {
this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]);
this.pageSubtitle = this.handleStringOrTranslation(firstChildRouteData["pageSubtitle"]);
}
if (firstChildRouteData["pageIcon"] !== undefined) {
@@ -101,19 +115,11 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
}
if (data.pageTitle) {
this.pageTitle = this.i18nService.t(data.pageTitle);
this.pageTitle = this.handleStringOrTranslation(data.pageTitle);
}
if (data.pageSubtitle) {
// If you pass just a string, we translate it by default
if (typeof data.pageSubtitle === "string") {
this.pageSubtitle = this.i18nService.t(data.pageSubtitle);
} else {
// if you pass an object, you can specify if you want to translate it or not
this.pageSubtitle = data.pageSubtitle.translate
? this.i18nService.t(data.pageSubtitle.subtitle)
: data.pageSubtitle.subtitle;
}
this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle);
}
if (data.pageIcon) {
@@ -129,6 +135,16 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.changeDetectorRef.detectChanges();
}
private handleStringOrTranslation(value: string | Translation): string {
if (typeof value === "string") {
// If it's a string, return it as is
return value;
}
// If it's a Translation object, translate it
return this.i18nService.t(value.key, ...(value.placeholders ?? []));
}
private resetPageData() {
this.pageTitle = null;
this.pageSubtitle = null;

View File

@@ -163,17 +163,20 @@ export const DefaultContentExample: Story = {
// Dynamic Content Example
const initialData: AnonLayoutWrapperData = {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
pageIcon: LockIcon,
};
const changedData: AnonLayoutWrapperData = {
pageTitle: "enterpriseSingleSignOn",
pageSubtitle: {
subtitle: "user@email.com (non-translated)",
translate: false,
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: "user@email.com (non-translated)",
pageIcon: RegistrationCheckEmailIcon,
};

View File

@@ -233,10 +233,7 @@ export class LockV2Component implements OnInit, OnDestroy {
private setEmailAsPageSubtitle(email: string) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageSubtitle: {
subtitle: email,
translate: false,
},
pageSubtitle: email,
});
}

View File

@@ -59,6 +59,24 @@ const INLINE_MENU_VISIBILITY = new KeyDefinition(
},
);
const SHOW_INLINE_MENU_IDENTITIES = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK,
"showInlineMenuIdentities",
{
deserializer: (value: boolean) => value ?? true,
clearOn: [],
},
);
const SHOW_INLINE_MENU_CARDS = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK,
"showInlineMenuCards",
{
deserializer: (value: boolean) => value ?? true,
clearOn: [],
},
);
const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableContextMenu", {
deserializer: (value: boolean) => value ?? true,
});
@@ -86,6 +104,10 @@ export abstract class AutofillSettingsServiceAbstraction {
setAutoCopyTotp: (newValue: boolean) => Promise<void>;
inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>;
setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise<void>;
showInlineMenuIdentities$: Observable<boolean>;
setShowInlineMenuIdentities: (newValue: boolean) => Promise<void>;
showInlineMenuCards$: Observable<boolean>;
setShowInlineMenuCards: (newValue: boolean) => Promise<void>;
enableContextMenu$: Observable<boolean>;
setEnableContextMenu: (newValue: boolean) => Promise<void>;
clearClipboardDelay$: Observable<ClearClipboardDelaySetting>;
@@ -113,6 +135,12 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
private inlineMenuVisibilityState: GlobalState<InlineMenuVisibilitySetting>;
readonly inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>;
private showInlineMenuIdentitiesState: ActiveUserState<boolean>;
readonly showInlineMenuIdentities$: Observable<boolean>;
private showInlineMenuCardsState: ActiveUserState<boolean>;
readonly showInlineMenuCards$: Observable<boolean>;
private enableContextMenuState: GlobalState<boolean>;
readonly enableContextMenu$: Observable<boolean>;
@@ -157,6 +185,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
map((x) => x ?? AutofillOverlayVisibility.Off),
);
this.showInlineMenuIdentitiesState = this.stateProvider.getActive(SHOW_INLINE_MENU_IDENTITIES);
this.showInlineMenuIdentities$ = this.showInlineMenuIdentitiesState.state$.pipe(
map((x) => x ?? true),
);
this.showInlineMenuCardsState = this.stateProvider.getActive(SHOW_INLINE_MENU_CARDS);
this.showInlineMenuCards$ = this.showInlineMenuCardsState.state$.pipe(map((x) => x ?? true));
this.enableContextMenuState = this.stateProvider.getGlobal(ENABLE_CONTEXT_MENU);
this.enableContextMenu$ = this.enableContextMenuState.state$.pipe(map((x) => x ?? true));
@@ -190,6 +226,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
await this.inlineMenuVisibilityState.update(() => newValue);
}
async setShowInlineMenuIdentities(newValue: boolean): Promise<void> {
await this.showInlineMenuIdentitiesState.update(() => newValue);
}
async setShowInlineMenuCards(newValue: boolean): Promise<void> {
await this.showInlineMenuCardsState.update(() => newValue);
}
async setEnableContextMenu(newValue: boolean): Promise<void> {
await this.enableContextMenuState.update(() => newValue);
}

View File

@@ -17,7 +17,7 @@ import { FieldView } from "../models/view/field.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
export abstract class CipherService implements UserKeyRotationDataProvider<CipherWithIdRequest> {
cipherViews$: Observable<Record<CipherId, CipherView>>;
cipherViews$: Observable<CipherView[]>;
ciphers$: Observable<Record<CipherId, CipherData>>;
localData$: Observable<Record<CipherId, LocalData>>;
/**

View File

@@ -1,5 +1,5 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject, map, of } from "rxjs";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
@@ -381,7 +381,7 @@ describe("Cipher Service", () => {
Cipher1: cipher1,
Cipher2: cipher2,
});
cipherService.cipherViews$ = decryptedCiphers;
cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers)));
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Cipher Key");

View File

@@ -1,4 +1,14 @@
import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs";
import {
combineLatest,
filter,
firstValueFrom,
map,
merge,
Observable,
shareReplay,
Subject,
switchMap,
} from "rxjs";
import { SemVer } from "semver";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -24,13 +34,7 @@ import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
ActiveUserState,
CIPHERS_MEMORY,
DeriveDefinition,
DerivedState,
StateProvider,
} from "../../platform/state";
import { ActiveUserState, StateProvider } from "../../platform/state";
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
import { OrgKey, UserKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
@@ -81,14 +85,25 @@ export class CipherService implements CipherServiceAbstraction {
private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(
this.sortCiphersByLastUsed,
);
private ciphersExpectingUpdate: DerivedState<boolean>;
/**
* Observable that forces the `cipherViews$` observable to re-emit with the provided value.
* Used to let subscribers of `cipherViews$` know that the decrypted ciphers have been cleared for the active user.
* @private
*/
private forceCipherViews$: Subject<CipherView[]> = new Subject<CipherView[]>();
localData$: Observable<Record<CipherId, LocalData>>;
ciphers$: Observable<Record<CipherId, CipherData>>;
cipherViews$: Observable<Record<CipherId, CipherView>>;
viewFor$(id: CipherId) {
return this.cipherViews$.pipe(map((views) => views[id]));
}
/**
* Observable that emits an array of decrypted ciphers for the active user.
* This observable will not emit until the encrypted ciphers have either been loaded from state or after sync.
*
* A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that
* decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete.
*
*/
cipherViews$: Observable<CipherView[] | null>;
addEditCipherInfo$: Observable<AddEditCipherInfo>;
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
@@ -115,23 +130,16 @@ export class CipherService implements CipherServiceAbstraction {
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
this.ciphersExpectingUpdate = this.stateProvider.getDerived(
this.encryptedCiphersState.state$,
new DeriveDefinition(CIPHERS_MEMORY, "ciphersExpectingUpdate", {
derive: (_: Record<CipherId, CipherData>) => false,
deserializer: (value) => value,
}),
{},
);
this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {}));
// First wait for ciphersExpectingUpdate to be false before emitting ciphers
this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe(
skipWhile((expectingUpdate) => expectingUpdate),
switchMap(() => this.encryptedCiphersState.state$),
map((ciphers) => ciphers ?? {}),
this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {}));
// Decrypted ciphers depend on both ciphers and local data and need to be updated when either changes
this.cipherViews$ = combineLatest([this.encryptedCiphersState.state$, this.localData$]).pipe(
filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())),
shareReplay({ bufferSize: 1, refCount: true }),
);
this.cipherViews$ = this.decryptedCiphersState.state$.pipe(map((views) => views ?? {}));
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
}
@@ -160,8 +168,14 @@ export class CipherService implements CipherServiceAbstraction {
}
async clearCache(userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
userId ??= activeUserId;
await this.clearDecryptedCiphersState(userId);
// Force the cipherView$ observable (which always tracks the active user) to re-emit
if (userId == activeUserId) {
this.forceCipherViews$.next(null);
}
}
async encrypt(
@@ -354,6 +368,11 @@ export class CipherService implements CipherServiceAbstraction {
return response;
}
/**
* Decrypts all ciphers for the active user and caches them in memory. If the ciphers have already been decrypted and
* cached, the cached ciphers are returned.
* @deprecated Use `cipherViews$` observable instead
*/
@sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise<CipherView[]> {
let decCiphers = await this.getDecryptedCiphers();
@@ -375,7 +394,9 @@ export class CipherService implements CipherServiceAbstraction {
}
private async getDecryptedCiphers() {
return Object.values(await firstValueFrom(this.cipherViews$));
return Object.values(
await firstValueFrom(this.decryptedCiphersState.state$.pipe(map((c) => c ?? {}))),
);
}
private async decryptCiphers(ciphers: Cipher[], userId: UserId) {
@@ -932,8 +953,6 @@ export class CipherService implements CipherServiceAbstraction {
userId: UserId = null,
): Promise<Record<CipherId, CipherData>> {
userId ||= await firstValueFrom(this.stateProvider.activeUserId$);
// Store that we should wait for an update to return any ciphers
await this.ciphersExpectingUpdate.forceValue(true);
await this.clearDecryptedCiphersState(userId);
const updatedCiphers = await this.stateProvider
.getUser(userId, ENCRYPTED_CIPHERS)
@@ -1254,7 +1273,7 @@ export class CipherService implements CipherServiceAbstraction {
let encryptedCiphers: CipherWithIdRequest[] = [];
const ciphers = await this.getAllDecrypted();
const ciphers = await firstValueFrom(this.cipherViews$);
if (!ciphers) {
return encryptedCiphers;
}

View File

@@ -17,6 +17,7 @@ or an options menu icon.
| <i class="bwi bwi-ban"></i> | bwi-ban | option or feature not available. Example: send maximum access count was reached |
| <i class="bwi bwi-check"></i> | bwi-check | confirmation action (Example: "confirm member"), successful confirmation (toast or callout), or shows currently selected option in a menu. Use with success color variable if applicable. |
| <i class="bwi bwi-error"></i> | bwi-error | error; used in form field error states and error toasts, banners, and callouts. Do not use as a close or clear icon. Use with danger color variable. |
| <i class="bwi bwi-expired"></i> | bwi-expired | - |
| <i class="bwi bwi-exclamation-circle"></i> | bwi-exclamation-circle | deprecated error icon; use bwi-error |
| <i class="bwi bwi-exclamation-triangle"></i> | bwi-exclamation-triangle | warning; used in warning callouts, banners, and toasts. Use with warning color variable. |
| <i class="bwi bwi-info-circle"></i> | bwi-info-circle | information; used in info callouts, banners, and toasts. Use with info color variable. |
@@ -25,21 +26,22 @@ or an options menu icon.
## Bitwarden Objects
| Icon | bwi-name | Usage |
| ----------------------------------- | --------------- | --------------------------------------------------- |
| <i class="bwi bwi-business"></i> | bwi-business | organization or vault for Free, Teams or Enterprise |
| <i class="bwi bwi-collection"></i> | bwi-collection | collection |
| <i class="bwi bwi-credit-card"></i> | bwi-credit-card | card item type |
| <i class="bwi bwi-family"></i> | bwi-family | family vault or organization |
| <i class="bwi bwi-folder"></i> | bwi-folder | folder |
| <i class="bwi bwi-globe"></i> | bwi-globe | login item type |
| <i class="bwi bwi-id-card"></i> | bwi-id-card | identity item type |
| <i class="bwi bwi-send"></i> | bwi-send | send action or feature |
| <i class="bwi bwi-send-f"></i> | bwi-send-f | - |
| <i class="bwi bwi-sticky-note"></i> | bwi-sticky-note | secure note item type |
| <i class="bwi bwi-users"></i> | bwi-users | user group |
| <i class="bwi bwi-vault"></i> | bwi-vault | general vault |
| <i class="bwi bwi-vault-f"></i> | bwi-vault-f | general vault |
| Icon | bwi-name | Usage |
| ------------------------------------- | ----------------- | --------------------------------------------------- |
| <i class="bwi bwi-authenticator"></i> | bwi-authenticator | authenticator app |
| <i class="bwi bwi-business"></i> | bwi-business | organization or vault for Free, Teams or Enterprise |
| <i class="bwi bwi-collection"></i> | bwi-collection | collection |
| <i class="bwi bwi-credit-card"></i> | bwi-credit-card | card item type |
| <i class="bwi bwi-family"></i> | bwi-family | family vault or organization |
| <i class="bwi bwi-folder"></i> | bwi-folder | folder |
| <i class="bwi bwi-globe"></i> | bwi-globe | login item type |
| <i class="bwi bwi-id-card"></i> | bwi-id-card | identity item type |
| <i class="bwi bwi-send"></i> | bwi-send | send action or feature |
| <i class="bwi bwi-send-f"></i> | bwi-send-f | - |
| <i class="bwi bwi-sticky-note"></i> | bwi-sticky-note | secure note item type |
| <i class="bwi bwi-users"></i> | bwi-users | user group |
| <i class="bwi bwi-vault"></i> | bwi-vault | general vault |
| <i class="bwi bwi-vault-f"></i> | bwi-vault-f | general vault |
## Actions
@@ -146,11 +148,21 @@ or an options menu icon.
| <i class="bwi bwi-file"></i> | bwi-file | file related objects or actions |
| <i class="bwi bwi-file-pdf"></i> | bwi-file-pdf | PDF related object or actions |
| <i class="bwi bwi-file-text"></i> | bwi-file-text | text related objects or actions |
| <i class="bwi bwi-fingerprint"></i> | bwi-fingerprint | - |
| <i class="bwi bwi-bw-folder-open-f1"></i> | bwi-bw-folder-open-f1 | - |
| <i class="bwi bwi-folder-closed-f"></i> | bwi-folder-closed-f | - |
| <i class="bwi bwi-folder-open"></i> | bwi-folder-open | - |
| <i class="bwi bwi-frown"></i> | bwi-frown | - |
| <i class="bwi bwi-hashtag"></i> | bwi-hashtag | link to specific id |
| <i class="bwi bwi-icon-1"></i> | bwi-icon-1 | - |
| <i class="bwi bwi-icon-2"></i> | bwi-icon-2 | - |
| <i class="bwi bwi-icon-3"></i> | bwi-icon-3 | - |
| <i class="bwi bwi-icon-4"></i> | bwi-icon-4 | - |
| <i class="bwi bwi-icon-5"></i> | bwi-icon-5 | - |
| <i class="bwi bwi-icon-6"></i> | bwi-icon-6 | - |
| <i class="bwi bwi-icon-7"></i> | bwi-icon-7 | - |
| <i class="bwi bwi-icon-8"></i> | bwi-icon-8 | - |
| <i class="bwi bwi-icon-9"></i> | bwi-icon-9 | - |
| <i class="bwi bwi-insurance"></i> | bwi-insurance | - |
| <i class="bwi bwi-key"></i> | bwi-key | key or password related objects or actions |
| <i class="bwi bwi-learning"></i> | bwi-learning | learning center |
@@ -178,6 +190,9 @@ or an options menu icon.
| <i class="bwi bwi-tag"></i> | bwi-tag | labels |
| <i class="bwi bwi-thumb-tack"></i> | bwi-thumb-tack | - |
| <i class="bwi bwi-thumbs-up"></i> | bwi-thumbs-up | - |
| <i class="bwi bwi-totp-codes"></i> | bwi-totp-codes | - |
| <i class="bwi bwi-totp-codes-alt"></i> | bwi-totp-codes-alt | - |
| <i class="bwi bwi-totp-codes-alt2"></i> | bwi-totp-codes-alt2 | - |
| <i class="bwi bwi-universal-access"></i> | bwi-universal-access | use for accessibility related actions |
| <i class="bwi bwi-user"></i> | bwi-user | relates to current user or organization member |
| <i class="bwi bwi-user-circle"></i> | bwi-user-circle | - |
@@ -189,27 +204,31 @@ or an options menu icon.
## Platforms and Logos
| Icon | bwi-name | Usage |
| --------------------------------- | ------------- | ---------------------------- |
| <i class="bwi bwi-android"></i> | bwi-android | android support |
| <i class="bwi bwi-apple"></i> | bwi-apple | apple/IOS support |
| <i class="bwi bwi-chrome"></i> | bwi-chrome | chrome support |
| <i class="bwi bwi-discourse"></i> | bwi-discourse | community forum |
| <i class="bwi bwi-edge"></i> | bwi-edge | edge support |
| <i class="bwi bwi-facebook"></i> | bwi-facebook | link to our facebook page |
| <i class="bwi bwi-firefox"></i> | bwi-firefox | support for firefox |
| <i class="bwi bwi-github"></i> | bwi-github | link to our github page |
| <i class="bwi bwi-google"></i> | bwi-google | link to our google page |
| <i class="bwi bwi-instagram"></i> | bwi-instagram | link to our Instagram page |
| <i class="bwi bwi-linkedin"></i> | bwi-linkedin | link to our linkedIn page |
| <i class="bwi bwi-linux"></i> | bwi-linux | linux support |
| <i class="bwi bwi-mastodon"></i> | bwi-mastodon | link to our Mastodon page |
| <i class="bwi bwi-opera"></i> | bwi-opera | support for Opera |
| <i class="bwi bwi-paypal"></i> | bwi-paypal | PayPal |
| <i class="bwi bwi-reddit"></i> | bwi-reddit | link to our reddit community |
| <i class="bwi bwi-safari"></i> | bwi-safari | safari support |
| <i class="bwi bwi-twitch"></i> | bwi-twitch | link to our Twitch page |
| <i class="bwi bwi-twitter"></i> | bwi-twitter | link to our twitter page |
| <i class="bwi bwi-windows"></i> | bwi-windows | support for windows |
| <i class="bwi bwi-x-twitter"></i> | bwi-x-twitter | x version of twitter |
| <i class="bwi bwi-youtube"></i> | bwi-youtube | link to our youtube page |
| Icon | bwi-name | Usage |
| ---------------------------------- | -------------- | ---------------------------- |
| <i class="bwi bwi-android"></i> | bwi-android | android support |
| <i class="bwi bwi-apple"></i> | bwi-apple | apple/IOS support |
| <i class="bwi bwi-brave"></i> | bwi-brave | - |
| <i class="bwi bwi-chrome"></i> | bwi-chrome | chrome support |
| <i class="bwi bwi-discourse"></i> | bwi-discourse | community forum |
| <i class="bwi bwi-duckduckgo"></i> | bwi-duckduckgo | - |
| <i class="bwi bwi-edge"></i> | bwi-edge | edge support |
| <i class="bwi bwi-facebook"></i> | bwi-facebook | link to our facebook page |
| <i class="bwi bwi-firefox"></i> | bwi-firefox | support for firefox |
| <i class="bwi bwi-github"></i> | bwi-github | link to our github page |
| <i class="bwi bwi-google"></i> | bwi-google | link to our google page |
| <i class="bwi bwi-instagram"></i> | bwi-instagram | link to our Instagram page |
| <i class="bwi bwi-linkedin"></i> | bwi-linkedin | link to our linkedIn page |
| <i class="bwi bwi-linux"></i> | bwi-linux | linux support |
| <i class="bwi bwi-mastodon"></i> | bwi-mastodon | link to our Mastodon page |
| <i class="bwi bwi-opera"></i> | bwi-opera | support for Opera |
| <i class="bwi bwi-paypal"></i> | bwi-paypal | PayPal |
| <i class="bwi bwi-reddit"></i> | bwi-reddit | link to our reddit community |
| <i class="bwi bwi-safari"></i> | bwi-safari | safari support |
| <i class="bwi bwi-twitch"></i> | bwi-twitch | link to our Twitch page |
| <i class="bwi bwi-twitter"></i> | bwi-twitter | link to our twitter page |
| <i class="bwi bwi-tor"></i> | bwi-tor | - |
| <i class="bwi bwi-vivaldi"></i> | bwi-vivaldi | - |
| <i class="bwi bwi-windows"></i> | bwi-windows | support for windows |
| <i class="bwi bwi-x-twitter"></i> | bwi-x-twitter | x version of twitter |
| <i class="bwi bwi-youtube"></i> | bwi-youtube | link to our youtube page |

View File

@@ -22,6 +22,7 @@
"@bitwarden/key-management": ["../key-management/src"],
"@bitwarden/platform": ["../platform/src"],
"@bitwarden/send-ui": ["../tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../tools/card/src"],
"@bitwarden/node/*": ["../node/src/*"],
"@bitwarden/vault": ["../vault/src"]
}

View File

@@ -0,0 +1,5 @@
## Tools Card
Package name: `@bitwarden/tools-card`
Generic Tools Card Component

View File

@@ -0,0 +1,13 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("../../../shared/tsconfig.libs");
/** @type {import('jest').Config} */
module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"],
preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/../../",
}),
};

View File

@@ -0,0 +1,24 @@
{
"name": "@bitwarden/tools-card",
"version": "0.0.0",
"description": "Angular card component",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/clients"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch"
},
"dependencies": {
"@bitwarden/common": "file:../../../common",
"@bitwarden/components": "file:../../../components"
}
}

View File

@@ -0,0 +1,7 @@
<div class="tw-flex-col">
<span bitTypography="body2" class="tw-flex tw-text-muted">{{ title }}</span>
<div class="tw-flex tw-items-baseline tw-gap-2">
<span bitTypography="h1">{{ value }}</span>
<span bitTypography="body2">{{ "cardMetrics" | i18n: maxValue }}</span>
</div>
</div>

View File

@@ -0,0 +1,30 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TypographyModule } from "@bitwarden/components";
@Component({
selector: "tools-card",
templateUrl: "./card.component.html",
standalone: true,
imports: [CommonModule, TypographyModule, JslibModule],
host: {
class:
"tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-p-6",
},
})
export class CardComponent {
/**
* The title of the card
*/
@Input() title: string;
/**
* The current value of the card as emphasized text
*/
@Input() value: number;
/**
* The maximum value of the card
*/
@Input() maxValue: number;
}

View File

@@ -0,0 +1,36 @@
import { CommonModule } from "@angular/common";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nMockService, TypographyModule } from "@bitwarden/components";
import { CardComponent } from "./card.component";
export default {
title: "Toools/Card",
component: CardComponent,
decorators: [
moduleMetadata({
imports: [CardComponent, CommonModule, TypographyModule],
providers: [
{
provide: I18nService,
useFactory: () =>
new I18nMockService({
cardMetrics: (value) => `out of ${value}`,
}),
},
],
}),
],
} as Meta;
type Story = StoryObj<CardComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<tools-card [title]="'Unsecured Members'" [value]="'38'" [maxValue]="'157'"></tools-card>`,
}),
};

View File

@@ -0,0 +1 @@
export { CardComponent } from "./card.component";

View File

@@ -0,0 +1 @@
import "jest-preset-angular/setup-jest";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../shared/tsconfig.libs",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"files": ["./test.setup.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -15,7 +15,7 @@
<div class="tw-grow tw-flex tw-items-center">
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
</div>
<div class="tw-space-x-1">
<div class="tw-flex tw-items-center tw-space-x-1">
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
{{ "generatePassword" | i18n }}
</button>

View File

@@ -20,6 +20,7 @@ import {
SectionHeaderComponent,
SelectModule,
ToggleGroupModule,
TypographyModule,
} from "@bitwarden/components";
import {
createRandomizer,
@@ -55,6 +56,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
SectionHeaderComponent,
SelectModule,
ToggleGroupModule,
TypographyModule,
],
providers: [
safeProvider({

View File

@@ -13,7 +13,7 @@
<div class="tw-grow tw-flex tw-items-center">
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
</div>
<div class="tw-space-x-1">
<div class="tw-flex tw-items-center tw-space-x-1">
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
{{ "generatePassword" | i18n }}
</button>

View File

@@ -1,6 +1,6 @@
<bit-section>
<bit-section-header *ngIf="showHeader">
<h6 bitTypography="h6">{{ "options" | i18n }}</h6>
<h2 bitTypography="h6">{{ "options" | i18n }}</h2>
</bit-section-header>
<form class="box" [formGroup]="settings" class="tw-container">
<div class="tw-mb-4">
@@ -55,7 +55,7 @@
</bit-form-control>
</div>
<div class="tw-flex">
<bit-form-field class="tw-basis-1/2 tw-mr-4">
<bit-form-field class="tw-w-full tw-basis-1/2 tw-mr-4">
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
<input
bitInput
@@ -65,7 +65,7 @@
formControlName="minNumber"
/>
</bit-form-field>
<bit-form-field class="tw-basis-1/2">
<bit-form-field class="tw-w-full tw-basis-1/2">
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
<input
bitInput

View File

@@ -2,7 +2,7 @@
<div class="tw-grow tw-flex tw-items-center">
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
</div>
<div class="tw-space-x-1">
<div class="tw-flex tw-items-center tw-space-x-1">
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
{{ "generatePassword" | i18n }}
</button>

View File

@@ -12,8 +12,8 @@
>
</bit-form-field>
<bit-form-field>
<bit-label *ngIf="!originalSendView || !hasPassword">{{ "password" | i18n }}</bit-label>
<bit-label *ngIf="originalSendView && hasPassword">{{ "newPassword" | i18n }}</bit-label>
<bit-label *ngIf="!shouldShowNewPassword">{{ "password" | i18n }}</bit-label>
<bit-label *ngIf="shouldShowNewPassword">{{ "newPassword" | i18n }}</bit-label>
<input bitInput type="password" formControlName="password" />
<button
data-testid="toggle-visibility-for-password"

View File

@@ -53,10 +53,8 @@ export class SendOptionsComponent implements OnInit {
hideEmail: [false as boolean],
});
get hasPassword(): boolean {
return (
this.sendOptionsForm.value.password !== null && this.sendOptionsForm.value.password !== ""
);
get shouldShowNewPassword(): boolean {
return this.originalSendView && this.originalSendView.password !== null;
}
get shouldShowCount(): boolean {

View File

@@ -1,6 +1,6 @@
import { TestBed } from "@angular/core/testing";
import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, first } from "rxjs";
import { BehaviorSubject } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -46,16 +46,6 @@ describe("SendListFiltersService", () => {
expect(service.sendTypes.map((c) => c.value)).toEqual([SendType.File, SendType.Text]);
});
it("filters disabled sends", (done) => {
const sends = [{ disabled: true }, { disabled: false }, { disabled: true }] as SendView[];
service.filterFunction$.pipe(first()).subscribe((filterFunction) => {
expect(filterFunction(sends)).toEqual([sends[1]]);
done();
});
service.filterForm.patchValue({});
});
it("resets the filter form", () => {
service.filterForm.patchValue({ sendType: SendType.Text });
service.resetFilterForm();

View File

@@ -44,11 +44,6 @@ export class SendListFiltersService {
map(
(filters) => (sends: SendView[]) =>
sends.filter((send) => {
// do not show disabled sends
if (send.disabled) {
return false;
}
if (filters.sendType !== null && send.type !== filters.sendType) {
return false;
}