1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 22:44:11 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Merissa Weinstein
2024-10-15 12:37:49 -05:00
84 changed files with 953 additions and 297 deletions

View File

@@ -293,6 +293,12 @@
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }]
}
},
{
"files": ["libs/tools/card/src/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/tools-card/*", "src/**/*"] }]
}
},
{
"files": ["libs/vault/src/**/*.ts"],
"rules": {

View File

@@ -14,6 +14,7 @@
./libs/tools/README.md
./libs/tools/export/vault-export/README.md
./libs/tools/send/README.md
./libs/tools/card/README.md
./libs/vault/README.md
./README.md
./LICENSE_BITWARDEN.txt

View File

@@ -19,6 +19,8 @@ const config: StorybookConfig = {
"../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)",
"../bitwarden_license/bit-web/src/**/*.mdx",
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
"../libs/tools/card/src/**/*.mdx",
"../libs/tools/card/src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
getAbsolutePath("@storybook/addon-links"),

View File

@@ -1408,6 +1408,12 @@
"showInlineMenuLabel": {
"message": "Show autofill suggestions on form fields"
},
"showInlineMenuIdentitiesLabel": {
"message": "Display identities as suggestions"
},
"showInlineMenuCardsLabel": {
"message": "Display cards as suggestions"
},
"showInlineMenuOnIconSelectionLabel": {
"message": "Display suggestions when icon is selected"
},
@@ -2647,6 +2653,15 @@
"message": "Your organization requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
"example": "5"
}
}
},
"verificationRequired": {
"message": "Verification required",
"description": "Default title for the user verification dialog."

View File

@@ -9,7 +9,7 @@ import {
AnonLayoutWrapperDataService,
} from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Icon, IconModule } from "@bitwarden/components";
import { Icon, IconModule, Translation } from "@bitwarden/components";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
@@ -90,11 +90,11 @@ export class ExtensionAnonLayoutWrapperComponent 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) {
@@ -132,19 +132,11 @@ export class ExtensionAnonLayoutWrapperComponent 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) {
@@ -168,6 +160,16 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
}
}
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

@@ -221,8 +221,12 @@ export const DefaultContentExample: Story = {
// Dynamic Content Example
const initialData: ExtensionAnonLayoutWrapperData = {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
pageIcon: LockIcon,
showAcctSwitcher: true,
showBackButton: true,
@@ -230,8 +234,12 @@ const initialData: ExtensionAnonLayoutWrapperData = {
};
const changedData: ExtensionAnonLayoutWrapperData = {
pageTitle: "enterpriseSingleSignOn",
pageSubtitle: "checkYourEmail",
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: {
key: "checkYourEmail",
},
pageIcon: RegistrationCheckEmailIcon,
showAcctSwitcher: false,
showBackButton: false,

View File

@@ -188,6 +188,8 @@ export type OverlayBackgroundExtensionMessageHandlers = {
updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void;
checkIsFieldCurrentlyFilling: () => boolean;
getAutofillInlineMenuVisibility: () => void;
getInlineMenuCardsVisibility: () => void;
getInlineMenuIdentitiesVisibility: () => void;
openAutofillInlineMenu: () => void;
closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void;

View File

@@ -132,6 +132,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message),
checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(),
getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(),
getInlineMenuCardsVisibility: () => this.getInlineMenuCardsVisibility(),
getInlineMenuIdentitiesVisibility: () => this.getInlineMenuIdentitiesVisibility(),
openAutofillInlineMenu: () => this.openInlineMenu(false),
closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message),
checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender),
@@ -365,7 +367,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
}
if (!this.cardAndIdentityCiphers.size) {
if (!this.cardAndIdentityCiphers?.size) {
this.cardAndIdentityCiphers = null;
}
@@ -1483,6 +1485,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$);
}
/**
* Gets the inline menu's visibility setting for Cards from the settings service.
*/
private async getInlineMenuCardsVisibility(): Promise<boolean> {
return await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$);
}
/**
* Gets the inline menu's visibility setting for Identities from the settings service.
*/
private async getInlineMenuIdentitiesVisibility(): Promise<boolean> {
return await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$);
}
/**
* Gets the user's authentication status from the auth service.
*/

View File

@@ -1,4 +1,5 @@
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { CipherType } from "@bitwarden/common/vault/enums";
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
@@ -23,7 +24,7 @@ export type AutofillExtensionMessage = {
data?: {
direction?: "previous" | "next" | "current";
forceCloseInlineMenu?: boolean;
inlineMenuVisibility?: number;
newSettingValue?: InlineMenuVisibilitySetting;
};
};

View File

@@ -14,7 +14,7 @@ describe("AutofillInlineMenuContentService", () => {
let autofillInlineMenuContentService: AutofillInlineMenuContentService;
let autofillInit: AutofillInit;
let sendExtensionMessageSpy: jest.SpyInstance;
let observeBodyMutationsSpy: jest.SpyInstance;
let observeContainerMutationsSpy: jest.SpyInstance;
const waitForIdleCallback = () =>
new Promise((resolve) => globalThis.requestIdleCallback(resolve));
@@ -25,8 +25,8 @@ describe("AutofillInlineMenuContentService", () => {
autofillInlineMenuContentService = new AutofillInlineMenuContentService();
autofillInit = new AutofillInit(domQueryService, null, autofillInlineMenuContentService);
autofillInit.init();
observeBodyMutationsSpy = jest.spyOn(
autofillInlineMenuContentService["bodyElementMutationObserver"] as any,
observeContainerMutationsSpy = jest.spyOn(
autofillInlineMenuContentService["containerElementMutationObserver"] as any,
"observe",
);
sendExtensionMessageSpy = jest.spyOn(
@@ -51,7 +51,7 @@ describe("AutofillInlineMenuContentService", () => {
describe("extension message handlers", () => {
describe("closeAutofillInlineMenu message handler", () => {
beforeEach(() => {
observeBodyMutationsSpy.mockImplementation();
observeContainerMutationsSpy.mockImplementation();
});
it("closes the inline menu button", async () => {
@@ -87,9 +87,9 @@ describe("AutofillInlineMenuContentService", () => {
});
it("closes both inline menu elements and removes the body element mutation observer", async () => {
const unobserveBodyElementSpy = jest.spyOn(
const unobserveContainerElementSpy = jest.spyOn(
autofillInlineMenuContentService as any,
"unobserveBodyElement",
"unobserveContainerElement",
);
sendMockExtensionMessage({
command: "appendAutofillInlineMenuToDom",
@@ -104,7 +104,7 @@ describe("AutofillInlineMenuContentService", () => {
command: "closeAutofillInlineMenu",
});
expect(unobserveBodyElementSpy).toHaveBeenCalled();
expect(unobserveContainerElementSpy).toHaveBeenCalled();
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
@@ -127,7 +127,7 @@ describe("AutofillInlineMenuContentService", () => {
.spyOn(autofillInlineMenuContentService as any, "isInlineMenuListVisible")
.mockResolvedValue(true);
jest.spyOn(globalThis.document.body, "appendChild");
observeBodyMutationsSpy.mockImplementation();
observeContainerMutationsSpy.mockImplementation();
});
describe("creating the inline menu button", () => {
@@ -279,7 +279,8 @@ describe("AutofillInlineMenuContentService", () => {
});
});
describe("handleBodyElementMutationObserverUpdate", () => {
describe("handleContainerElementMutationObserverUpdate", () => {
let mockMutationRecord: MockProxy<MutationRecord>;
let buttonElement: HTMLElement;
let listElement: HTMLElement;
let isInlineMenuListVisibleSpy: jest.SpyInstance;
@@ -289,6 +290,7 @@ describe("AutofillInlineMenuContentService", () => {
<div class="overlay-button"></div>
<div class="overlay-list"></div>
`;
mockMutationRecord = mock<MutationRecord>({ target: globalThis.document.body } as any);
buttonElement = document.querySelector(".overlay-button") as HTMLElement;
listElement = document.querySelector(".overlay-list") as HTMLElement;
autofillInlineMenuContentService["buttonElement"] = buttonElement;
@@ -309,7 +311,9 @@ describe("AutofillInlineMenuContentService", () => {
autofillInlineMenuContentService["buttonElement"] = undefined;
autofillInlineMenuContentService["listElement"] = undefined;
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
@@ -323,7 +327,9 @@ describe("AutofillInlineMenuContentService", () => {
)
.mockReturnValue(true);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
@@ -332,14 +338,18 @@ describe("AutofillInlineMenuContentService", () => {
it("skips re-arranging the DOM elements if the last child of the body is non-existent", async () => {
document.body.innerHTML = "";
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
});
it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", async () => {
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
@@ -349,7 +359,9 @@ describe("AutofillInlineMenuContentService", () => {
listElement.remove();
isInlineMenuListVisibleSpy.mockResolvedValue(false);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
@@ -359,7 +371,9 @@ describe("AutofillInlineMenuContentService", () => {
const injectedElement = document.createElement("div");
document.body.insertBefore(injectedElement, listElement);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
@@ -371,7 +385,9 @@ describe("AutofillInlineMenuContentService", () => {
it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", async () => {
document.body.appendChild(buttonElement);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
@@ -384,7 +400,9 @@ describe("AutofillInlineMenuContentService", () => {
const injectedElement = document.createElement("div");
document.body.appendChild(injectedElement);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith(
@@ -409,7 +427,9 @@ describe("AutofillInlineMenuContentService", () => {
1000,
);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([
mockMutationRecord,
]);
await waitForIdleCallback();
expect(persistentLastChild.style.getPropertyValue("z-index")).toBe("2147483646");

View File

@@ -30,7 +30,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private buttonElement: HTMLElement;
private listElement: HTMLElement;
private inlineMenuElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver;
private containerElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout;
@@ -102,7 +102,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
return;
}
this.unobserveBodyElement();
this.unobserveContainerElement();
this.closeInlineMenuButton();
this.closeInlineMenuList();
};
@@ -153,7 +153,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
if (!(await this.isInlineMenuButtonVisible())) {
this.appendInlineMenuElementToBody(this.buttonElement);
this.appendInlineMenuElementToDom(this.buttonElement);
this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true);
}
}
@@ -168,7 +168,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
if (!(await this.isInlineMenuListVisible())) {
this.appendInlineMenuElementToBody(this.listElement);
this.appendInlineMenuElementToDom(this.listElement);
this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true);
}
}
@@ -196,8 +196,15 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
*
* @param element - The inline menu element to append to the body element.
*/
private appendInlineMenuElementToBody(element: HTMLElement) {
this.observeBodyElement();
private appendInlineMenuElementToDom(element: HTMLElement) {
const parentDialogElement = globalThis.document.activeElement?.closest("dialog");
if (parentDialogElement && parentDialogElement.open && parentDialogElement.matches(":modal")) {
this.observeContainerElement(parentDialogElement);
parentDialogElement.appendChild(element);
return;
}
this.observeContainerElement(globalThis.document.body);
globalThis.document.body.appendChild(element);
}
@@ -276,8 +283,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
this.handleInlineMenuElementMutationObserverUpdate,
);
this.bodyElementMutationObserver = new MutationObserver(
this.handleBodyElementMutationObserverUpdate,
this.containerElementMutationObserver = new MutationObserver(
this.handleContainerElementMutationObserverUpdate,
);
};
@@ -306,19 +313,17 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
/**
* Sets up a mutation observer for the body element. The mutation observer is used
* to ensure that the inline menu elements are always present at the bottom of the
* body element.
* Sets up a mutation observer for the element which contains the inline menu.
*/
private observeBodyElement() {
this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
private observeContainerElement(element: HTMLElement) {
this.containerElementMutationObserver?.observe(element, { childList: true });
}
/**
* Disconnects the mutation observer for the body element.
* Disconnects the mutation observer for the element which contains the inline menu.
*/
private unobserveBodyElement() {
this.bodyElementMutationObserver?.disconnect();
private unobserveContainerElement() {
this.containerElementMutationObserver?.disconnect();
}
/**
@@ -370,11 +375,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
/**
* Handles the mutation observer update for the body element. This method will
* ensure that the inline menu elements are always present at the bottom of the
* body element.
* Handles the mutation observer update for the element that contains the inline menu.
* This method will ensure that the inline menu elements are always present at the
* bottom of the container.
*/
private handleBodyElementMutationObserverUpdate = () => {
private handleContainerElementMutationObserverUpdate = (mutations: MutationRecord[]) => {
if (
(!this.buttonElement && !this.listElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
@@ -382,15 +387,18 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
return;
}
requestIdleCallbackPolyfill(this.processBodyElementMutation, { timeout: 500 });
const containerElement = mutations[0].target as HTMLElement;
requestIdleCallbackPolyfill(() => this.processContainerElementMutation(containerElement), {
timeout: 500,
});
};
/**
* Processes the mutation of the body element. Will trigger when an
* Processes the mutation of the element that contains the inline menu. Will trigger when an
* idle moment in the execution of the main thread is detected.
*/
private processBodyElementMutation = async () => {
const lastChild = globalThis.document.body.lastElementChild;
private processContainerElementMutation = async (containerElement: HTMLElement) => {
const lastChild = containerElement.lastElementChild;
const secondToLastChild = lastChild?.previousElementSibling;
const lastChildIsInlineMenuList = lastChild === this.listElement;
const lastChildIsInlineMenuButton = lastChild === this.buttonElement;
@@ -424,11 +432,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
(lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) ||
(lastChildIsInlineMenuButton && isInlineMenuListVisible)
) {
globalThis.document.body.insertBefore(this.buttonElement, this.listElement);
containerElement.insertBefore(this.buttonElement, this.listElement);
return;
}
globalThis.document.body.insertBefore(lastChild, this.buttonElement);
containerElement.insertBefore(lastChild, this.buttonElement);
};
/**

View File

@@ -46,6 +46,32 @@
</div>
</div>
</div>
<div class="box tw-mb-5" *ngIf="inlineMenuPositioningImprovementsEnabled && inlineMenuIsEnabled">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="show-inline-menu-identities" class="!tw-mr-0">{{
"showInlineMenuIdentitiesLabel" | i18n
}}</label>
<input
id="show-inline-menu-identities"
type="checkbox"
(change)="updateShowInlineMenuIdentities()"
[(ngModel)]="showInlineMenuIdentities"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="show-inline-menu-cards" class="!tw-mr-0">{{
"showInlineMenuCardsLabel" | i18n
}}</label>
<input
id="show-inline-menu-cards"
type="checkbox"
(change)="updateShowInlineMenuCards()"
[(ngModel)]="showInlineMenuCards"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-content" *ngIf="canOverrideBrowserAutofillSetting">
<div class="box-content-row box-content-row-checkbox" appBoxRow>

View File

@@ -8,10 +8,12 @@ import {
InlineMenuVisibilitySetting,
ClearClipboardDelaySetting,
} from "@bitwarden/common/autofill/types";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -20,7 +22,6 @@ import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import { AutofillService } from "../../services/abstractions/autofill.service";
@Component({
selector: "app-autofill-v1",
@@ -32,6 +33,10 @@ export class AutofillV1Component implements OnInit {
protected autoFillOverlayVisibility: InlineMenuVisibilitySetting;
protected autoFillOverlayVisibilityOptions: any[];
protected disablePasswordManagerLink: string;
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
protected showInlineMenuIdentities: boolean = true;
protected showInlineMenuCards: boolean = true;
inlineMenuIsEnabled: boolean = false;
enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[];
@@ -50,7 +55,7 @@ export class AutofillV1Component implements OnInit {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private domainSettingsService: DomainSettingsService,
private autofillService: AutofillService,
private configService: ConfigService,
private dialogService: DialogService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private messagingService: MessagingService,
@@ -109,6 +114,20 @@ export class AutofillV1Component implements OnInit {
this.autofillSettingsService.inlineMenuVisibility$,
);
this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.InlineMenuPositioningImprovements,
);
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
this.showInlineMenuIdentities =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
this.showInlineMenuCards =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$));
this.enableAutoFillOnPageLoad = await firstValueFrom(
this.autofillSettingsService.autofillOnPageLoad$,
);
@@ -140,9 +159,18 @@ export class AutofillV1Component implements OnInit {
);
}
isInlineMenuEnabled() {
return (
this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus ||
this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick
);
}
async updateAutoFillOverlayVisibility() {
await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility);
await this.requestPrivacyPermission();
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
}
async updateAutoFillOnPageLoad() {
@@ -298,4 +326,12 @@ export class AutofillV1Component implements OnInit {
async updateShowIdentitiesCurrentTab() {
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
}
async updateShowInlineMenuCards() {
await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards);
}
async updateShowInlineMenuIdentities() {
await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities);
}
}

View File

@@ -27,7 +27,37 @@
{{ "showInlineMenuOnFormFieldsDescAlt" | i18n }}
</bit-hint>
</bit-form-control>
<bit-form-control *ngIf="enableInlineMenu" class="tw-pl-5">
<bit-form-control
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
class="tw-ml-5"
>
<input
bitCheckbox
id="show-inline-menu-identities"
type="checkbox"
(change)="updateShowInlineMenuIdentities()"
[(ngModel)]="showInlineMenuIdentities"
/>
<bit-label for="show-inline-menu-identities">
{{ "showInlineMenuIdentitiesLabel" | i18n }}
</bit-label>
</bit-form-control>
<bit-form-control
*ngIf="inlineMenuPositioningImprovementsEnabled && enableInlineMenu"
class="tw-ml-5"
>
<input
bitCheckbox
id="show-inline-menu-cards"
type="checkbox"
(change)="updateShowInlineMenuCards()"
[(ngModel)]="showInlineMenuCards"
/>
<bit-label for="show-inline-menu-cards">
{{ "showInlineMenuCardsLabel" | i18n }}
</bit-label>
</bit-form-control>
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
<input
bitCheckbox
id="show-autofill-suggestions-on-icon"

View File

@@ -21,10 +21,12 @@ import {
DisablePasswordManagerUri,
InlineMenuVisibilitySetting,
} from "@bitwarden/common/autofill/types";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -82,6 +84,7 @@ export class AutofillComponent implements OnInit {
protected defaultBrowserAutofillDisabled: boolean = false;
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
AutofillOverlayVisibility.OnFieldFocus;
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
protected disablePasswordManagerURI: DisablePasswordManagerUri =
DisablePasswordManagerUris.Unknown;
@@ -93,6 +96,8 @@ export class AutofillComponent implements OnInit {
enableAutofillOnPageLoad: boolean = false;
enableInlineMenu: boolean = false;
enableInlineMenuOnIconSelect: boolean = false;
showInlineMenuIdentities: boolean = true;
showInlineMenuCards: boolean = true;
autofillOnPageLoadDefault: boolean = false;
autofillOnPageLoadOptions: { name: string; value: boolean }[];
enableContextMenuItem: boolean = false;
@@ -114,6 +119,7 @@ export class AutofillComponent implements OnInit {
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private messagingService: MessagingService,
private vaultSettingsService: VaultSettingsService,
private configService: ConfigService,
) {
this.autofillOnPageLoadOptions = [
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
@@ -151,6 +157,18 @@ export class AutofillComponent implements OnInit {
this.autofillSettingsService.inlineMenuVisibility$,
);
this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.InlineMenuPositioningImprovements,
);
this.showInlineMenuIdentities =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
this.showInlineMenuCards =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$));
this.enableInlineMenuOnIconSelect =
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick;
@@ -381,4 +399,12 @@ export class AutofillComponent implements OnInit {
async updateShowIdentitiesCurrentTab() {
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
}
async updateShowInlineMenuCards() {
await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards);
}
async updateShowInlineMenuIdentities() {
await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities);
}
}

View File

@@ -2238,7 +2238,7 @@ describe("AutofillOverlayContentService", () => {
it("updates the inlineMenuVisibility property", () => {
sendMockExtensionMessage({
command: "updateAutofillInlineMenuVisibility",
data: { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
data: { newSettingValue: AutofillOverlayVisibility.OnButtonClick },
});
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(

View File

@@ -8,7 +8,9 @@ import {
AutofillOverlayVisibility,
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT,
AUTOFILL_OVERLAY_HANDLE_SCROLL,
} from "@bitwarden/common/autofill/constants";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { CipherType } from "@bitwarden/common/vault/enums";
import {
@@ -51,7 +53,9 @@ import { AutoFillConstants } from "./autofill-constants";
export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface {
pageDetailsUpdateRequired = false;
inlineMenuVisibility: number;
inlineMenuVisibility: InlineMenuVisibilitySetting;
private showInlineMenuIdentities: boolean;
private showInlineMenuCards: boolean;
private readonly findTabs = tabbable;
private readonly sendExtensionMessage = sendExtensionMessage;
private formFieldElements: Map<ElementWithOpId<FormFieldElement>, AutofillField> = new Map();
@@ -183,6 +187,18 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
autofillFieldData: AutofillField,
pageDetails: AutofillPageDetails,
) {
if (!this.inlineMenuVisibility) {
await this.getInlineMenuVisibility();
}
if (this.showInlineMenuCards == null) {
await this.getInlineMenuCardsVisibility();
}
if (this.showInlineMenuIdentities == null) {
await this.getInlineMenuIdentitiesVisibility();
}
if (
this.formFieldElements.has(formFieldElement) ||
this.isIgnoredField(autofillFieldData, pageDetails)
@@ -1019,10 +1035,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
const { width, height, top, left } =
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
const autofillFieldData = this.formFieldElements.get(formFieldElement);
let accountCreationFieldType = null;
if (
// user setting allows display of identities in inline menu
this.showInlineMenuIdentities &&
// `showInlineMenuAccountCreation` has been set or field is filled by Login cipher
(autofillFieldData?.showInlineMenuAccountCreation ||
autofillFieldData?.filledByCipherType === CipherType.Login) &&
// field is a username field, which is relevant to both Identity and Login ciphers
this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)
) {
accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField(
@@ -1125,6 +1147,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (
this.showInlineMenuCards &&
this.inlineMenuFieldQualificationService.isFieldForCreditCardForm(
autofillFieldData,
pageDetails,
@@ -1135,6 +1158,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (
this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,
@@ -1146,6 +1170,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (
this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForIdentityForm(
autofillFieldData,
pageDetails,
@@ -1244,6 +1269,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled");
autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled");
autofillFieldData.viewable = true;
void this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData);
}
@@ -1266,10 +1292,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
await this.updateMostRecentlyFocusedField(formFieldElement);
}
if (!this.inlineMenuVisibility) {
await this.getInlineMenuVisibility();
}
this.setupFormFieldElementEventListeners(formFieldElement);
this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData);
@@ -1291,6 +1313,30 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus;
}
/**
* Queries the background script for the autofill inline menu's Cards visibility setting.
* If the setting is not found, a default value of true will be used
* @private
*/
private async getInlineMenuCardsVisibility() {
const inlineMenuCardsVisibility = await this.sendExtensionMessage(
"getInlineMenuCardsVisibility",
);
this.showInlineMenuCards = inlineMenuCardsVisibility ?? true;
}
/**
* Queries the background script for the autofill inline menu's Identities visibility setting.
* If the setting is not found, a default value of true will be used
* @private
*/
private async getInlineMenuIdentitiesVisibility() {
const inlineMenuIdentitiesVisibility = await this.sendExtensionMessage(
"getInlineMenuIdentitiesVisibility",
);
this.showInlineMenuIdentities = inlineMenuIdentitiesVisibility ?? true;
}
/**
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
*
@@ -1318,8 +1364,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* @param data - The data object from the extension message.
*/
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
if (!isNaN(data?.inlineMenuVisibility)) {
this.inlineMenuVisibility = data.inlineMenuVisibility;
const newSettingValue = data?.newSettingValue;
if (!isNaN(newSettingValue)) {
this.inlineMenuVisibility = newSettingValue;
}
}
@@ -1600,15 +1648,28 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* the overlay elements on scroll or resize.
*/
private setOverlayRepositionEventListeners() {
const handler = this.useEventHandlersMemo(
const repositionHandler = this.useEventHandlersMemo(
throttle(this.handleOverlayRepositionEvent, 250),
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
);
globalThis.addEventListener(EVENTS.SCROLL, handler, {
const eventTargetDoesNotContainFocusedField = (element: Element) =>
typeof element?.contains === "function" && !element.contains(this.mostRecentlyFocusedField);
const scrollHandler = this.useEventHandlersMemo(
throttle((event) => {
if (eventTargetDoesNotContainFocusedField(event.target as Element)) {
return;
}
repositionHandler(event);
}, 50),
AUTOFILL_OVERLAY_HANDLE_SCROLL,
);
globalThis.addEventListener(EVENTS.SCROLL, scrollHandler, {
capture: true,
passive: true,
});
globalThis.addEventListener(EVENTS.RESIZE, handler);
globalThis.addEventListener(EVENTS.RESIZE, repositionHandler);
}
/**
@@ -1616,12 +1677,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* the overlay elements on scroll or resize.
*/
private removeOverlayRepositionEventListeners() {
const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION];
globalThis.removeEventListener(EVENTS.SCROLL, handler, {
capture: true,
});
globalThis.removeEventListener(EVENTS.RESIZE, handler);
globalThis.removeEventListener(
EVENTS.SCROLL,
this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL],
{
capture: true,
},
);
globalThis.removeEventListener(
EVENTS.RESIZE,
this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION],
);
delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL];
delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION];
}

View File

@@ -75,6 +75,8 @@ describe("AutofillService", () => {
let autofillService: AutofillService;
const cipherService = mock<CipherService>();
let inlineMenuVisibilityMock$!: BehaviorSubject<InlineMenuVisibilitySetting>;
let showInlineMenuCardsMock$!: BehaviorSubject<boolean>;
let showInlineMenuIdentitiesMock$!: BehaviorSubject<boolean>;
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@@ -98,8 +100,12 @@ describe("AutofillService", () => {
beforeEach(() => {
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
showInlineMenuCardsMock$ = new BehaviorSubject(false);
showInlineMenuIdentitiesMock$ = new BehaviorSubject(false);
autofillSettingsService = mock<AutofillSettingsServiceAbstraction>();
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$;
autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$;
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
authService = mock<AuthService>();
authService.activeAccountStatus$ = activeAccountStatusMock$;
@@ -291,12 +297,12 @@ describe("AutofillService", () => {
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
tab1,
"updateAutofillInlineMenuVisibility",
{ inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
{ newSettingValue: AutofillOverlayVisibility.OnButtonClick },
);
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
tab2,
"updateAutofillInlineMenuVisibility",
{ inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
{ newSettingValue: AutofillOverlayVisibility.OnButtonClick },
);
});
@@ -308,12 +314,12 @@ describe("AutofillService", () => {
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
tab1,
"updateAutofillInlineMenuVisibility",
{ inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus },
{ newSettingValue: AutofillOverlayVisibility.OnFieldFocus },
);
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
tab2,
"updateAutofillInlineMenuVisibility",
{ inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus },
{ newSettingValue: AutofillOverlayVisibility.OnFieldFocus },
);
});
});

View File

@@ -130,10 +130,23 @@ export default class AutofillService implements AutofillServiceInterface {
async loadAutofillScriptsOnInstall() {
BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection);
void this.injectAutofillScriptsInAllTabs();
this.autofillSettingsService.inlineMenuVisibility$
.pipe(startWith(undefined), pairwise())
.subscribe(([previousSetting, currentSetting]) =>
this.handleInlineMenuVisibilityChange(previousSetting, currentSetting),
this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting),
);
this.autofillSettingsService.showInlineMenuCards$
.pipe(startWith(undefined), pairwise())
.subscribe(([previousSetting, currentSetting]) =>
this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting),
);
this.autofillSettingsService.showInlineMenuIdentities$
.pipe(startWith(undefined), pairwise())
.subscribe(([previousSetting, currentSetting]) =>
this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting),
);
}
@@ -3043,27 +3056,36 @@ export default class AutofillService implements AutofillServiceInterface {
}
/**
* Updates the autofill inline menu visibility setting in all active tabs
* when the InlineMenuVisibilitySetting observable is updated.
* Updates the autofill inline menu visibility settings in all active tabs
* when the inlineMenuVisibility, showInlineMenuCards, or showInlineMenuIdentities
* observables are updated.
*
* @param previousSetting - The previous setting value
* @param currentSetting - The current setting value
* @param oldSettingValue - The previous setting value
* @param newSettingValue - The current setting value
* @param cipherType - The cipher type of the changed inline menu setting
*/
private async handleInlineMenuVisibilityChange(
previousSetting: InlineMenuVisibilitySetting,
currentSetting: InlineMenuVisibilitySetting,
private async handleInlineMenuVisibilitySettingsChange(
oldSettingValue: InlineMenuVisibilitySetting | boolean,
newSettingValue: InlineMenuVisibilitySetting | boolean,
) {
if (previousSetting === undefined || previousSetting === currentSetting) {
if (oldSettingValue === undefined || oldSettingValue === newSettingValue) {
return;
}
const inlineMenuPreviouslyDisabled = previousSetting === AutofillOverlayVisibility.Off;
const inlineMenuCurrentlyDisabled = currentSetting === AutofillOverlayVisibility.Off;
if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) {
const isInlineMenuVisibilitySubSetting =
typeof oldSettingValue === "boolean" || typeof newSettingValue === "boolean";
const inlineMenuPreviouslyDisabled = oldSettingValue === AutofillOverlayVisibility.Off;
const inlineMenuCurrentlyDisabled = newSettingValue === AutofillOverlayVisibility.Off;
if (
!isInlineMenuVisibilitySubSetting &&
!inlineMenuPreviouslyDisabled &&
!inlineMenuCurrentlyDisabled
) {
const tabs = await BrowserApi.tabsQuery({});
tabs.forEach((tab) =>
BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", {
inlineMenuVisibility: currentSetting,
newSettingValue,
}),
);
return;

View File

@@ -1340,7 +1340,7 @@ export default class MainBackground {
}
if (!supported) {
this.sdkService.failedToInitialize().catch(this.logService.error);
this.sdkService.failedToInitialize().catch((e) => this.logService.error(e));
}
}

View File

@@ -449,7 +449,9 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: "yourVaultIsLockedV2",
pageTitle: {
key: "yourVaultIsLockedV2",
},
showReadonlyHostname: true,
showAcctSwitcher: true,
} satisfies ExtensionAnonLayoutWrapperData,
@@ -471,7 +473,9 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
state: "signup",
pageTitle: "createAccount",
pageTitle: {
key: "createAccount",
},
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
@@ -492,8 +496,12 @@ const routes: Routes = [
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
state: "finish-signup",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -508,8 +516,12 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
component: SetPasswordJitComponent,
data: {
pageTitle: "joinOrganization",
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
state: "set-password-jit",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},

View File

@@ -81,7 +81,7 @@ export class AppComponent implements OnInit, OnDestroy {
.subscribe((supported) => {
if (!supported) {
this.logService.debug("SDK is not supported");
this.sdkService.failedToInitialize().catch(this.logService.error);
this.sdkService.failedToInitialize().catch((e) => this.logService.error(e));
} else {
this.logService.debug("SDK is supported");
}

View File

@@ -1,4 +1,5 @@
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ItemModule } from "@bitwarden/components";
@@ -22,6 +23,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
PopupHeaderComponent,
PopupPageComponent,
PopupFooterComponent,
RouterModule,
ItemModule,
],
})

View File

@@ -9,7 +9,6 @@
<bit-item>
<button type="button" bit-item-content (click)="about()">
{{ "aboutBitwarden" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button>
</bit-item>
<bit-item>

View File

@@ -32,6 +32,7 @@
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/vault": ["../../libs/vault/src"]
},

View File

@@ -80,7 +80,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldts": "6.1.48",
"tldts": "6.1.51",
"zxcvbn": "4.4.2"
}
}

View File

@@ -858,7 +858,7 @@ export class ServiceContainer {
}
if (!supported) {
this.sdkService.failedToInitialize().catch(this.logService.error);
this.sdkService.failedToInitialize().catch((e) => this.logService.error(e));
}
}
}

View File

@@ -10,7 +10,7 @@ publish = false
anyhow = "=1.0.86"
desktop_core = { path = "../core", default-features = false }
futures = "0.3.30"
log = "0.4.21"
log = "0.4.22"
simplelog = "0.12.2"
tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] }
tokio-util = { version = "0.7.11", features = ["codec"] }

View File

@@ -18,7 +18,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.16.10",
"@types/node": "20.16.11",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
}
@@ -106,9 +106,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"version": "20.16.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"

View File

@@ -23,7 +23,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.16.10",
"@types/node": "20.16.11",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
},

View File

@@ -141,8 +141,12 @@ const routes: Routes = [
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "requestPasswordHint",
pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
pageTitle: {
key: "requestPasswordHint",
},
pageSubtitle: {
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
},
pageIcon: UserLockIcon,
} satisfies AnonLayoutWrapperData,
children: [
@@ -164,7 +168,11 @@ const routes: Routes = [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData,
data: {
pageTitle: {
key: "createAccount",
},
} satisfies AnonLayoutWrapperData,
children: [
{
path: "",
@@ -184,8 +192,12 @@ const routes: Routes = [
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
} satisfies AnonLayoutWrapperData,
children: [
{
@@ -199,7 +211,9 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: "yourVaultIsLockedV2",
pageTitle: {
key: "yourVaultIsLockedV2",
},
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
children: [
@@ -214,8 +228,12 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
component: SetPasswordJitComponent,
data: {
pageTitle: "joinOrganization",
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
} satisfies AnonLayoutWrapperData,
},
],

View File

@@ -167,7 +167,7 @@ export class AppComponent implements OnInit, OnDestroy {
.subscribe((supported) => {
if (!supported) {
this.logService.debug("SDK is not supported");
this.sdkService.failedToInitialize().catch(this.logService.error);
this.sdkService.failedToInitialize().catch((e) => this.logService.error(e));
} else {
this.logService.debug("SDK is supported");
}

View File

@@ -1752,6 +1752,15 @@
"message": "Your organization requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
"example": "5"
}
}
},
"verificationRequired": {
"message": "Verification required",
"description": "Default title for the user verification dialog."

View File

@@ -32,6 +32,7 @@
"@bitwarden/node/*": ["../../libs/node/src/*"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/vault": ["../../libs/vault/src"]
},
"useDefineForClassFields": false

View File

@@ -105,7 +105,7 @@ export class AppComponent implements OnDestroy, OnInit {
.subscribe((supported) => {
if (!supported) {
this.logService.debug("SDK is not supported");
this.sdkService.failedToInitialize().catch(this.logService.error);
this.sdkService.failedToInitialize().catch((e) => this.logService.error(e));
} else {
this.logService.debug("SDK is supported");
}

View File

@@ -14,22 +14,24 @@ describe("freeTrialTextResolver", () => {
it("shows password manager text", () => {
route.queryParams.product = `${ProductType.PasswordManager}`;
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(
"continueSettingUpFreeTrialPasswordManager",
);
expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({
key: "continueSettingUpFreeTrialPasswordManager",
});
});
it("shows secret manager text", () => {
route.queryParams.product = `${ProductType.SecretsManager}`;
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(
"continueSettingUpFreeTrialSecretsManager",
);
expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({
key: "continueSettingUpFreeTrialSecretsManager",
});
});
it("shows default text", () => {
route.queryParams.product = `${ProductType.PasswordManager},${ProductType.SecretsManager}`;
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe("continueSettingUpFreeTrial");
expect(freeTrialTextResolver(route, routerStateSnapshot)).toEqual({
key: "continueSettingUpFreeTrial",
});
});
});

View File

@@ -1,10 +1,11 @@
import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router";
import { ProductType } from "@bitwarden/common/billing/enums";
import { Translation } from "@bitwarden/components";
export const freeTrialTextResolver: ResolveFn<string | null> = (
export const freeTrialTextResolver: ResolveFn<Translation | null> = (
route: ActivatedRouteSnapshot,
): string | null => {
): Translation | null => {
const { product } = route.queryParams;
const products: ProductType[] = (product ?? "").split(",").map((p: string) => parseInt(p));
@@ -13,10 +14,16 @@ export const freeTrialTextResolver: ResolveFn<string | null> = (
switch (true) {
case onlyPasswordManager:
return "continueSettingUpFreeTrialPasswordManager";
return {
key: "continueSettingUpFreeTrialPasswordManager",
};
case onlySecretsManager:
return "continueSettingUpFreeTrialSecretsManager";
return {
key: "continueSettingUpFreeTrialSecretsManager",
};
default:
return "continueSettingUpFreeTrial";
return {
key: "continueSettingUpFreeTrial",
};
}
};

View File

@@ -228,7 +228,9 @@ const routes: Routes = [
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "createAccount",
pageTitle: {
key: "createAccount",
},
titleId: "createAccount",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -250,8 +252,12 @@ const routes: Routes = [
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
titleId: "setAStrongPassword",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -264,7 +270,9 @@ const routes: Routes = [
{
path: "send/:sendId/:key",
data: {
pageTitle: "viewSend",
pageTitle: {
key: "viewSend",
},
showReadonlyHostname: true,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -284,15 +292,21 @@ const routes: Routes = [
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
component: SetPasswordJitComponent,
data: {
pageTitle: "joinOrganization",
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
pageTitle: {
key: "joinOrganization",
},
pageSubtitle: {
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
},
} satisfies AnonLayoutWrapperData,
},
{
path: "signup-link-expired",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "expiredLink",
pageTitle: {
key: "expiredLink",
},
} satisfies AnonLayoutWrapperData,
children: [
{
@@ -308,7 +322,9 @@ const routes: Routes = [
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "enterpriseSingleSignOn",
pageTitle: {
key: "enterpriseSingleSignOn",
},
titleId: "enterpriseSingleSignOn",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -338,7 +354,9 @@ const routes: Routes = [
},
],
data: {
pageTitle: "logIn",
pageTitle: {
key: "logIn",
},
},
},
...extensionRefreshSwap(
@@ -354,7 +372,9 @@ const routes: Routes = [
},
],
data: {
pageTitle: "yourVaultIsLockedV2",
pageTitle: {
key: "yourVaultIsLockedV2",
},
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
@@ -369,7 +389,9 @@ const routes: Routes = [
},
],
data: {
pageTitle: "yourAccountIsLocked",
pageTitle: {
key: "yourAccountIsLocked",
},
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
@@ -390,7 +412,9 @@ const routes: Routes = [
},
],
data: {
pageTitle: "verifyIdentity",
pageTitle: {
key: "verifyIdentity",
},
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
{
@@ -408,7 +432,9 @@ const routes: Routes = [
},
],
data: {
pageTitle: "recoverAccountTwoStep",
pageTitle: {
key: "recoverAccountTwoStep",
},
titleId: "recoverAccountTwoStep",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
@@ -416,7 +442,9 @@ const routes: Routes = [
path: "accept-emergency",
canActivate: [deepLinkGuard()],
data: {
pageTitle: "emergencyAccess",
pageTitle: {
key: "emergencyAccess",
},
titleId: "acceptEmergency",
doNotSaveUrl: false,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
@@ -434,7 +462,9 @@ const routes: Routes = [
path: "recover-delete",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "deleteAccount",
pageTitle: {
key: "deleteAccount",
},
titleId: "deleteAccount",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -453,7 +483,9 @@ const routes: Routes = [
path: "verify-recover-delete",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "deleteAccount",
pageTitle: {
key: "deleteAccount",
},
titleId: "deleteAccount",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@@ -468,7 +500,9 @@ const routes: Routes = [
component: RemovePasswordComponent,
canActivate: [authGuard],
data: {
pageTitle: "removeMasterPassword",
pageTitle: {
key: "removeMasterPassword",
},
titleId: "removeMasterPassword",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},

View File

@@ -157,8 +157,8 @@ export class AccessComponent implements OnInit {
if (this.creatorIdentifier != null) {
this.layoutWrapperDataService.setAnonLayoutWrapperData({
pageSubtitle: {
subtitle: this.i18nService.t("sendAccessCreatorIdentifier", this.creatorIdentifier),
translate: false,
key: "sendAccessCreatorIdentifier",
placeholders: [this.creatorIdentifier],
},
});
}

View File

@@ -1,5 +1,6 @@
<td bitCell [ngClass]="RowHeightClass" class="tw-min-w-fit">
<input
*ngIf="showCheckbox"
type="checkbox"
bitCheckbox
appStopProp

View File

@@ -42,6 +42,7 @@ export class VaultCipherRowComponent implements OnInit {
@Output() checkedToggled = new EventEmitter<void>();
protected CipherType = CipherType;
protected organization?: Organization;
constructor(private configService: ConfigService) {}
@@ -53,6 +54,9 @@ export class VaultCipherRowComponent implements OnInit {
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
if (this.cipher.organizationId != null) {
this.organization = this.organizations.find((o) => o.id === this.cipher.organizationId);
}
}
protected get showTotpCopyButton() {
@@ -138,4 +142,12 @@ export class VaultCipherRowComponent implements OnInit {
protected assignToCollections() {
this.onEvent.emit({ type: "assignToCollections", items: [this.cipher] });
}
protected get showCheckbox() {
if (!this.viewingOrgVault || !this.organization) {
return true; // Always show checkbox in individual vault or for non-org items
}
return this.organization.canEditAllCiphers || this.cipher.edit;
}
}

View File

@@ -103,6 +103,10 @@ export class VaultCollectionRowComponent {
}
protected get showCheckbox() {
return this.collection?.id !== Unassigned;
if (this.collection?.id === Unassigned) {
return false; // Never show checkbox for Unassigned
}
return this.canEditCollection || this.canDeleteCollection;
}
}

View File

@@ -1,7 +1,7 @@
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Unassigned, CollectionView } from "@bitwarden/admin-console/common";
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { TableDataSource } from "@bitwarden/components";
@@ -205,11 +205,12 @@ export class VaultItemsComponent {
this.selection.clear();
// Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission
// All ciphers are selectable, collections only if they can be edited or deleted
this.editableItems = items.filter(
(item) =>
item.cipher !== undefined ||
(item.collection !== undefined && item.collection.id !== Unassigned),
(item.collection !== undefined &&
(this.canEditCollection(item.collection) || this.canDeleteCollection(item.collection))),
);
this.dataSource.data = items;

View File

@@ -7922,6 +7922,15 @@
"message": "Your organization requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
"example": "5"
}
}
},
"notFound": {
"message": "$RESOURCE$ not found",
"placeholders": {

View File

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

View File

@@ -21,6 +21,7 @@
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/vault": ["../../libs/vault/src"],

View File

@@ -22,7 +22,7 @@
<bit-table>
<ng-template body>
<ng-container *ngIf="subscription">
<tr bitRow *ngFor="let i of subscription.plans">
<tr bitRow *ngFor="let i of activePlans">
<td bitCell class="tw-pl-0 tw-py-3">
{{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{
i.cadence.toLowerCase()

View File

@@ -101,4 +101,14 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
this.destroy$.next();
this.destroy$.complete();
}
get activePlans(): ProviderPlanResponse[] {
return this.subscription.plans.filter((plan) => {
if (plan.purchasedSeats === 0) {
return plan.seatMinimum > 0;
} else {
return plan.purchasedSeats > 0;
}
});
}
}

View File

@@ -26,6 +26,7 @@
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],

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

@@ -58,6 +58,8 @@ export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds
export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event";
export const AUTOFILL_OVERLAY_HANDLE_SCROLL = "autofill-overlay-handle-scroll-event";
export const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll";
export const AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT = "autofill-trigger-form-field-submit";

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,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;
}

34
package-lock.json generated
View File

@@ -58,7 +58,7 @@
"node-fetch": "2.6.12",
"node-forge": "1.3.1",
"nord": "0.2.1",
"oidc-client-ts": "2.4.0",
"oidc-client-ts": "2.4.1",
"open": "8.4.2",
"papaparse": "5.4.1",
"patch-package": "8.0.0",
@@ -68,7 +68,7 @@
"qrious": "4.0.2",
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.48",
"tldts": "6.1.51",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"
@@ -110,7 +110,7 @@
"@types/koa-json": "2.0.23",
"@types/lowdb": "1.0.15",
"@types/lunr": "2.3.7",
"@types/node": "20.16.10",
"@types/node": "20.16.11",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
@@ -225,7 +225,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldts": "6.1.48",
"tldts": "6.1.51",
"zxcvbn": "4.4.2"
},
"bin": {
@@ -9646,9 +9646,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"version": "20.16.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -29879,9 +29879,9 @@
"license": "MIT"
},
"node_modules/oidc-client-ts": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz",
"integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.1.tgz",
"integrity": "sha512-IxlGMsbkZPsHJGCliWT3LxjUcYzmiN21656n/Zt2jDncZlBFc//cd8WqFF0Lt681UT3AImM57E6d4N53ziTCYA==",
"license": "Apache-2.0",
"dependencies": {
"crypto-js": "^4.2.0",
@@ -36519,21 +36519,21 @@
}
},
"node_modules/tldts": {
"version": "6.1.48",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.48.tgz",
"integrity": "sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==",
"version": "6.1.51",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz",
"integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.48"
"tldts-core": "^6.1.51"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.50",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.50.tgz",
"integrity": "sha512-na2EcZqmdA2iV9zHV7OHQDxxdciEpxrjbkp+aHmZgnZKHzoElLajP59np5/4+sare9fQBfixgvXKx8ev1d7ytw==",
"version": "6.1.51",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz",
"integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==",
"license": "MIT"
},
"node_modules/tmp": {

View File

@@ -71,7 +71,7 @@
"@types/koa-json": "2.0.23",
"@types/lowdb": "1.0.15",
"@types/lunr": "2.3.7",
"@types/node": "20.16.10",
"@types/node": "20.16.11",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
@@ -192,7 +192,7 @@
"node-fetch": "2.6.12",
"node-forge": "1.3.1",
"nord": "0.2.1",
"oidc-client-ts": "2.4.0",
"oidc-client-ts": "2.4.1",
"open": "8.4.2",
"papaparse": "5.4.1",
"patch-package": "8.0.0",
@@ -202,7 +202,7 @@
"qrious": "4.0.2",
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.48",
"tldts": "6.1.51",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"

View File

@@ -31,6 +31,7 @@
"@bitwarden/importer/core": ["./libs/importer/src"],
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/send-ui": [".libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": [".libs/tools/card/src"],
"@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/vault": ["./libs/vault/src"],

View File

@@ -34,6 +34,7 @@
"@bitwarden/key-management": ["./libs/key-management/src"],
"@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["./libs/tools/card/src"],
"@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/web-vault/*": ["./apps/web/src/*"],
"@bitwarden/vault": ["./libs/vault/src"],
@@ -51,6 +52,7 @@
"apps/browser/src/**/*",
"libs/*/src/**/*",
"libs/tools/send/**/src/**/*",
"libs/tools/card/src/**/*",
"bitwarden_license/bit-web/src/**/*",
"bitwarden_license/bit-common/src/**/*"
],