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:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 321 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -233,10 +233,7 @@ export class LockV2Component implements OnInit, OnDestroy {
|
||||
|
||||
private setEmailAsPageSubtitle(email: string) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageSubtitle: {
|
||||
subtitle: email,
|
||||
translate: false,
|
||||
},
|
||||
pageSubtitle: email,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
5
libs/tools/card/README.md
Normal file
5
libs/tools/card/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Tools Card
|
||||
|
||||
Package name: `@bitwarden/tools-card`
|
||||
|
||||
Generic Tools Card Component
|
||||
13
libs/tools/card/jest.config.js
Normal file
13
libs/tools/card/jest.config.js
Normal 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>/../../",
|
||||
}),
|
||||
};
|
||||
24
libs/tools/card/package.json
Normal file
24
libs/tools/card/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
libs/tools/card/src/card.component.html
Normal file
7
libs/tools/card/src/card.component.html
Normal 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>
|
||||
30
libs/tools/card/src/card.component.ts
Normal file
30
libs/tools/card/src/card.component.ts
Normal 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;
|
||||
}
|
||||
36
libs/tools/card/src/card.stories.ts
Normal file
36
libs/tools/card/src/card.stories.ts
Normal 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>`,
|
||||
}),
|
||||
};
|
||||
1
libs/tools/card/src/index.ts
Normal file
1
libs/tools/card/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CardComponent } from "./card.component";
|
||||
1
libs/tools/card/test.setup.ts
Normal file
1
libs/tools/card/test.setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "jest-preset-angular/setup-jest";
|
||||
5
libs/tools/card/tsconfig.json
Normal file
5
libs/tools/card/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../shared/tsconfig.libs",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
6
libs/tools/card/tsconfig.spec.json
Normal file
6
libs/tools/card/tsconfig.spec.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"files": ["./test.setup.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user