1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

Merge branch 'main' of github.com:bitwarden/clients

This commit is contained in:
gbubemismith
2024-04-30 21:01:54 -04:00
48 changed files with 1064 additions and 172 deletions

View File

@@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { BrowserApi } from "../../platform/browser/browser-api";
export default class WebRequestBackground {
private pendingAuthRequests: any[] = [];
private webRequest: any;
private pendingAuthRequests: Set<string> = new Set<string>([]);
private isFirefox: boolean;
constructor(
platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private authService: AuthService,
private readonly webRequest: typeof chrome.webRequest,
) {
if (BrowserApi.isManifestVersion(2)) {
this.webRequest = chrome.webRequest;
}
this.isFirefox = platformUtilsService.isFirefox();
}
async init() {
if (!this.webRequest || !this.webRequest.onAuthRequired) {
return;
}
startListening() {
this.webRequest.onAuthRequired.addListener(
async (details: any, callback: any) => {
if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
async (details, callback) => {
if (!details.url || this.pendingAuthRequests.has(details.requestId)) {
if (callback) {
callback();
callback(null);
}
return;
}
this.pendingAuthRequests.push(details.requestId);
this.pendingAuthRequests.add(details.requestId);
if (this.isFirefox) {
// eslint-disable-next-line
return new Promise(async (resolve, reject) => {
@@ -51,7 +40,7 @@ export default class WebRequestBackground {
[this.isFirefox ? "blocking" : "asyncBlocking"],
);
this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), {
urls: ["http://*/*"],
});
this.webRequest.onErrorOccurred.addListener(
@@ -91,10 +80,7 @@ export default class WebRequestBackground {
}
}
private completeAuthRequest(details: any) {
const i = this.pendingAuthRequests.indexOf(details.requestId);
if (i > -1) {
this.pendingAuthRequests.splice(i, 1);
}
private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) {
this.pendingAuthRequests.delete(details.requestId);
}
}

View File

@@ -1056,11 +1056,12 @@ export default class MainBackground {
this.cipherService,
);
if (BrowserApi.isManifestVersion(2)) {
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService,
this.authService,
chrome.webRequest,
);
}
}
@@ -1106,9 +1107,7 @@ export default class MainBackground {
await this.tabsBackground.init();
this.contextMenusBackground?.init();
await this.idleBackground.init();
if (BrowserApi.isManifestVersion(2)) {
await this.webRequestBackground.init();
}
this.webRequestBackground?.startListening();
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
// Set Private Mode windows to the default icon - they do not share state with the background page

View File

@@ -76,7 +76,8 @@ export default class RuntimeBackground {
void this.processMessageWithSender(msg, sender).catch((err) =>
this.logService.error(
`Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`,
`Error while processing message in RuntimeBackground '${msg?.command}'.`,
err,
),
);
return false;

View File

@@ -60,7 +60,9 @@
"clipboardWrite",
"idle",
"scripting",
"offscreen"
"offscreen",
"webRequest",
"webRequestAuthProvider"
],
"optional_permissions": ["nativeMessaging", "privacy"],
"host_permissions": ["<all_urls>"],

View File

@@ -28,6 +28,7 @@ describe("OffscreenDocument", () => {
});
it("shows a console message if the handler throws an error", async () => {
const error = new Error("test error");
browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error"));
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" });
@@ -35,7 +36,8 @@ describe("OffscreenDocument", () => {
expect(browserClipboardServiceCopySpy).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error resolving extension message response: Error: test error",
"Error resolving extension message response",
error,
);
});

View File

@@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface {
Promise.resolve(messageResponse)
.then((response) => sendResponse(response))
.catch((error) =>
this.consoleLogService.error(`Error resolving extension message response: ${error}`),
this.consoleLogService.error("Error resolving extension message response", error),
);
return true;
};

View File

@@ -6,9 +6,11 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
AvatarModule,
BadgeModule,
ButtonModule,
I18nMockService,
IconButtonModule,
ItemModule,
} from "@bitwarden/components";
import { PopupFooterComponent } from "./popup-footer.component";
@@ -30,23 +32,34 @@ class ExtensionContainerComponent {}
@Component({
selector: "vault-placeholder",
template: `
<div class="tw-mb-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item last item</div>
<bit-item-group aria-label="Mock Vault Items">
<bit-item *ngFor="let item of data; index as i">
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
{{ i }} of {{ data.length - 1 }}
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" aria-label="Copy item"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
`,
standalone: true,
imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule],
})
class VaultComponent {}
class VaultComponent {
protected data = Array.from(Array(20).keys());
}
@Component({
selector: "generator-placeholder",

View File

@@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec";
import { ConsoleLogService } from "./console-log.service";
let caughtMessage: any = {};
describe("CLI Console log service", () => {
const error = new Error("this is an error");
const obj = { a: 1, b: 2 };
let logService: ConsoleLogService;
let consoleSpy: {
log: jest.Mock<any, any>;
warn: jest.Mock<any, any>;
error: jest.Mock<any, any>;
};
beforeEach(() => {
caughtMessage = {};
interceptConsole(caughtMessage);
consoleSpy = interceptConsole();
logService = new ConsoleLogService(true);
});
@@ -19,24 +24,21 @@ describe("CLI Console log service", () => {
it("should redirect all console to error if BW_RESPONSE env is true", () => {
process.env.BW_RESPONSE = "true";
logService.debug("this is a debug message");
expect(caughtMessage).toMatchObject({
error: { 0: "this is a debug message" },
});
logService.debug("this is a debug message", error, obj);
expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj);
});
it("should not redirect console to error if BW_RESPONSE != true", () => {
process.env.BW_RESPONSE = "false";
logService.debug("debug");
logService.info("info");
logService.warning("warning");
logService.error("error");
logService.debug("debug", error, obj);
logService.info("info", error, obj);
logService.warning("warning", error, obj);
logService.error("error", error, obj);
expect(caughtMessage).toMatchObject({
log: { 0: "info" },
warn: { 0: "warning" },
error: { 0: "error" },
});
expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj);
expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj);
expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj);
expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj);
});
});

View File

@@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService {
super(isDev, filter);
}
write(level: LogLevelType, message: string) {
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
if (process.env.BW_RESPONSE === "true") {
// eslint-disable-next-line
console.error(message);
console.error(message, ...optionalParams);
return;
}
super.write(level, message);
super.write(level, message, ...optionalParams);
}
}

View File

@@ -103,7 +103,8 @@ export default {
isMacAppStore: isMacAppStore(),
isWindowsStore: isWindowsStore(),
reloadProcess: () => ipcRenderer.send("reload-process"),
log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }),
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
openContextMenu: (
menu: {

View File

@@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService {
}
log.initialize();
ipcMain.handle("ipc.log", (_event, { level, message }) => {
this.write(level, message);
ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => {
this.write(level, message, ...optionalParams);
});
}
write(level: LogLevelType, message: string) {
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
switch (level) {
case LogLevelType.Debug:
log.debug(message);
log.debug(message, ...optionalParams);
break;
case LogLevelType.Info:
log.info(message);
log.info(message, ...optionalParams);
break;
case LogLevelType.Warning:
log.warn(message);
log.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
log.error(message);
log.error(message, ...optionalParams);
break;
default:
break;

View File

@@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService {
super(ipc.platform.isDev, filter);
}
write(level: LogLevelType, message: string) {
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
/* eslint-disable no-console */
ipc.platform.log(level, message).catch((e) => console.log("Error logging", e));
ipc.platform
.log(level, message, ...optionalParams)
.catch((e) => console.log("Error logging", e));
/* eslint-disable no-console */
switch (level) {
case LogLevelType.Debug:
console.debug(message);
console.debug(message, ...optionalParams);
break;
case LogLevelType.Info:
console.info(message);
console.info(message, ...optionalParams);
break;
case LogLevelType.Warning:
console.warn(message);
console.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
console.error(message);
console.error(message, ...optionalParams);
break;
default:
break;

View File

@@ -0,0 +1,25 @@
import { svgIcon } from "@bitwarden/components";
export const ManageBilling = svgIcon`
<svg width="213" height="231" viewBox="0 0 213 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.089 85.6617C129.868 85.4299 129.604 85.2456 129.31 85.1197C129.016 84.9937 128.7 84.9299 128.381 84.9317H84.5811C84.2617 84.9299 83.9441 84.9937 83.6503 85.1197C83.3565 85.2456 83.0919 85.4299 82.8729 85.6617C82.6411 85.8807 82.4568 86.1471 82.3308 86.441C82.2049 86.7348 82.141 87.0505 82.1429 87.3699V116.57C82.152 118.793 82.5827 120.994 83.4131 123.056C84.2033 125.091 85.2654 127.011 86.5703 128.761C87.9117 130.515 89.4137 132.137 91.0562 133.612C92.58 135.01 94.186 136.318 95.8632 137.528C97.3232 138.565 98.8562 139.547 100.462 140.474C102.068 141.401 103.202 142.027 103.864 142.353C104.532 142.682 105.072 142.941 105.474 143.113C105.788 143.264 106.132 143.339 106.481 143.332C106.824 143.337 107.164 143.257 107.47 143.102C107.879 142.923 108.412 142.671 109.087 142.343C109.762 142.014 110.912 141.386 112.489 140.463C114.066 139.539 115.617 138.554 117.088 137.517C118.767 136.305 120.375 134.999 121.902 133.601C123.547 132.128 125.049 130.504 126.388 128.75C127.691 126.998 128.753 125.08 129.545 123.045C130.378 120.983 130.808 118.782 130.816 116.559V87.3589C130.817 87.0414 130.754 86.7275 130.628 86.4355C130.502 86.1435 130.319 85.8807 130.089 85.6617ZM124.443 116.836C124.443 127.421 106.481 136.513 106.481 136.513V91.1878H124.443V116.836Z" fill="#212529"/>
<path d="M62.7328 163.392C62.7328 168.149 51.6616 166.263 46.761 166.263C41.8605 166.263 22.5074 161.096 20.7328 153.058C23.6946 151.005 16.0004 143.298 31.9722 142.724C33.1529 141.759 44.9083 148.712 46.761 149.039C51.6616 149.039 62.7328 158.636 62.7328 163.392Z" fill="#E5E5E5"/>
<path d="M21.3544 122.3C21.4472 123.4 22.4147 124.217 23.5153 124.125C24.616 124.032 25.433 123.064 25.3402 121.964L21.3544 122.3ZM148.234 45.7444C149.303 45.4678 149.946 44.3767 149.669 43.3073L145.162 25.8808C144.885 24.8114 143.794 24.1687 142.725 24.4453C141.655 24.7219 141.013 25.813 141.289 26.8824L145.296 42.3726L129.805 46.3792C128.736 46.6558 128.093 47.7469 128.37 48.8163C128.647 49.8857 129.738 50.5283 130.807 50.2517L148.234 45.7444ZM25.3402 121.964C23.4116 99.0873 31.1986 75.5542 48.6989 58.0539L45.8705 55.2255C27.5023 73.5937 19.331 98.2998 21.3544 122.3L25.3402 121.964ZM48.6989 58.0539C75.2732 31.4796 115.769 27.3025 146.718 45.5314L148.748 42.0848C116.267 22.9532 73.7654 27.3305 45.8705 55.2255L48.6989 58.0539Z" fill="#212529"/>
<path d="M64.2075 185.062C63.1417 185.352 62.5129 186.451 62.8029 187.517L67.5298 204.885C67.8199 205.951 68.919 206.58 69.9848 206.29C71.0507 205.999 71.6795 204.9 71.3895 203.834L67.1878 188.396L82.6262 184.194C83.692 183.904 84.3209 182.805 84.0308 181.739C83.7408 180.674 82.6416 180.045 81.5758 180.335L64.2075 185.062ZM189.211 100.283C189.018 99.1952 187.98 98.4697 186.893 98.6625C185.805 98.8552 185.08 99.8931 185.272 100.981L189.211 100.283ZM162.871 172.225C136.546 198.55 96.5599 202.897 65.726 185.255L63.7396 188.727C96.0997 207.242 138.066 202.687 165.699 175.054L162.871 172.225ZM185.272 100.981C189.718 126.07 182.249 152.847 162.871 172.225L165.699 175.054C186.04 154.713 193.875 126.603 189.211 100.283L185.272 100.981Z" fill="#212529"/>
<path d="M34.4588 108.132C36.0159 92.1931 42.8984 76.6765 55.1062 64.4686C72.0222 47.5527 95.2911 40.8618 117.233 44.396" stroke="#212529" stroke-width="2" stroke-linecap="round"/>
<path d="M177.328 119.132C176.386 136.119 169.426 152.834 156.449 165.811C141.173 181.088 120.715 188.025 100.733 186.623" stroke="#212529" stroke-width="2" stroke-linecap="round"/>
<rect x="150.233" y="56.1318" width="49" height="34" rx="2.5" stroke="#212529" stroke-width="3"/>
<path d="M150.233 63.6318V63.6318C150.233 66.9455 152.919 69.6318 156.233 69.6318H169.242M199.233 63.6318V63.6318C199.233 66.9455 196.546 69.6318 193.233 69.6318H180.224" stroke="#212529" stroke-width="3"/>
<mask id="path-9-inside-1_873_6447" fill="white">
<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25"/>
</mask>
<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25" stroke="#212529" stroke-width="6" mask="url(#path-9-inside-1_873_6447)"/>
<path d="M183.733 54.6318C183.733 54.6318 183.733 53.6318 183.733 52.6318C183.733 51.6318 182.785 50.6318 181.838 50.6318C180.891 50.6318 168.575 50.6318 167.628 50.6318C166.68 50.6318 165.733 51.6318 165.733 52.6318C165.733 53.6318 165.733 54.6318 165.733 54.6318" stroke="#212529" stroke-width="3"/>
<circle cx="48.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M65.7263 170.132H65.6454H65.5646H65.484H65.4036H65.3233H65.2432H65.1632H65.0834H65.0037H64.9242H64.8449H64.7657H64.6866H64.6077H64.529H64.4504H64.372H64.2937H64.2155H64.1376H64.0597H63.982H63.9045H63.8271H63.7498H63.6727H63.5957H63.5189H63.4422H63.3657H63.2893H63.213H63.1369H63.0609H62.985H62.9093H62.8338H62.7583H62.683H62.6079H62.5329H62.458H62.3832H62.3086H62.2341H62.1597H62.0855H62.0114H61.9374H61.8636H61.7899H61.7163H61.6428H61.5695H61.4963H61.4232H61.3503H61.2774H61.2047H61.1321H61.0597H60.9873H60.9151H60.843H60.771H60.6992H60.6274H60.5558H60.4843H60.4129H60.3416H60.2704H60.1994H60.1284H60.0576H59.9869H59.9163H59.8458H59.7754H59.7052H59.635H59.5649H59.495H59.4252H59.3554H59.2858H59.2163H59.1469H59.0776H59.0084H58.9393H58.8703H58.8013H58.7325H58.6638H58.5952H58.5267H58.4583H58.39H58.3218H58.2537H58.1857H58.1178H58.0499H57.9822H57.9146H57.847H57.7796H57.7122H57.6449H57.5777H57.5106H57.4436H57.3767H57.3099H57.2431H57.1765H57.1099H57.0434H56.977H56.9107H56.8444H56.7783H56.7122H56.6462H56.5803H56.5145H56.4487H56.383H56.3174H56.2519H56.1865H56.1211H56.0558H55.9906H55.9254H55.8603H55.7953H55.7304H55.6655H55.6008H55.536H55.4714H55.4068H55.3423H55.2778H55.2135H55.1492H55.0849H55.0207H54.9566H54.8925H54.8286H54.7646H54.7008H54.6369H54.5732H54.5095H54.4459H54.3823H54.3188H54.2553H54.1919H54.1286H54.0653H54.0021H53.9389H53.8758H53.8127H53.7497H53.6867H53.6238H53.5609H53.4981H53.4353H53.3726H53.3099H53.2473H53.1847H53.1222H53.0597H52.9972H52.9348H52.8725H52.8102H52.7479H52.6856H52.6234H52.5613H52.4992H52.4371H52.375H52.313H52.2511H52.1891H52.1272H52.0654H52.0036H51.9418H51.88H51.8183H51.7566H51.6949H51.6333H51.5717H51.5101H51.4485H51.387H51.3255H51.264H51.2026H51.1412H51.0798H51.0184H50.9571H50.8957H50.8344H50.7731H50.7119H50.6506H50.5894H50.5282H50.467H50.4058H50.3447H50.2836H50.2224H50.1613H50.1002H50.0392H49.9781H49.917H49.856H49.795H49.7339H49.6729H49.6119H49.5509H49.4899H49.429H49.368H49.307H49.246H49.1851H49.1241H49.0632H49.0022H48.9413H48.8803H48.8194H48.7584H48.6975H48.6365H48.5756H48.5146H48.4537H48.3927H48.3318H48.2708H48.2098H48.1488H48.0878H48.0268H47.9658H47.9048H47.8438H47.7828H47.7217H47.6607H47.5996H47.5385H47.4774H47.4163H47.3552H47.294H47.2329H47.1717H47.1105H47.0493H46.9881H46.9268H46.8656H46.8043H46.743H46.6816H46.6203H46.5589H46.4975H46.4361H46.3746H46.3132H46.2517H46.1901H46.1286H46.067H46.0054H45.9437H45.8821H45.8203H45.7586H45.6968H45.635H45.5732H45.5113H45.4494H45.3875H45.3255H45.2635H45.2015H45.1394H45.0772H45.0151H44.9529H44.8906H44.8283H44.766H44.7036H44.6412H44.5788H44.5163H44.4537H44.3911H44.3285H44.2658H44.2031H44.1403H44.0775H44.0146H43.9517H43.8887H43.8256H43.7626H43.6994H43.6362H43.573H43.5097H43.4463H43.3829H43.3195H43.2559H43.1924H43.1287H43.065H43.0013H42.9374H42.8736H42.8096H42.7456H42.6815H42.6174H42.5532H42.4889H42.4246H42.3602H42.2958H42.2312H42.1666H42.102H42.0373H41.9724H41.9076H41.8426H41.7776H41.7125H41.6474H41.5821H41.5168H41.4514H41.386H41.3204H41.2548H41.1891H41.1233H41.0575H40.9916H40.9255H40.8594H40.7933H40.727H40.6607H40.5943H40.5277H40.4612H40.3945H40.3277H40.2609H40.1939H40.1269H40.0598H39.9926H39.9253H39.8579H39.7904H39.7229H39.6552H39.5874H39.5196H39.4517H39.3836H39.3155H39.2473H39.1789H39.1105H39.042H38.9734H38.9046H38.8358H38.7669H38.6979H38.6288H38.5595H38.4902H38.4208H38.3512H38.2816H38.2118H38.142H38.072H38.0019H37.9317H37.8615H37.7911H37.7205H37.6499H37.5792H37.5083H37.4374H37.3663H37.2951H37.2238H37.1524H37.0809H37.0092H36.9374H36.8655H36.7935H36.7214H36.6492H36.5768H36.5043H36.4317H36.359H36.2861H36.2131H36.14H36.0668H35.9934H35.9199H35.8463H35.7726H35.6987H35.6247H35.5506H35.4764H35.402H35.3274H35.2528H35.178H35.1031H35.028H34.9528H34.8775H34.8021H34.7265H34.6507H34.5749H34.4989H34.4227H34.3464H34.27H34.1934H34.1167H34.0398H33.9628H33.8857H33.8084H33.731H33.6534H33.5757H33.4978H33.4198H33.3416H33.2633H33.1848H33.1062H33.0274H32.9485H32.8694H32.7902H32.7108H32.6313H32.5516H32.4718H32.3918H32.3116H32.2313H32.1508H32.0702H31.9894H31.9085H31.8273H31.7461H31.6646H31.583H31.5013H31.4194H31.3373H31.255H31.1726H31.09H31.0073H30.9243C30.7817 170.132 30.7021 170.098 30.6492 170.065C30.5881 170.026 30.5107 169.954 30.4348 169.823C30.2689 169.538 30.1936 169.112 30.2525 168.743C31.6563 159.954 39.3802 153.206 48.7252 153.206C58.0703 153.206 65.7943 159.954 67.198 168.743C67.3079 169.431 67.1364 169.686 67.0452 169.781C66.9216 169.91 66.5692 170.132 65.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/>
<circle cx="20.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M37.7263 170.132H37.6454H37.5646H37.484H37.4036H37.3233H37.2432H37.1632H37.0834H37.0037H36.9242H36.8449H36.7657H36.6866H36.6077H36.529H36.4504H36.372H36.2937H36.2155H36.1376H36.0597H35.982H35.9045H35.8271H35.7498H35.6727H35.5957H35.5189H35.4422H35.3657H35.2893H35.213H35.1369H35.0609H34.985H34.9093H34.8338H34.7583H34.683H34.6079H34.5329H34.458H34.3832H34.3086H34.2341H34.1597H34.0855H34.0114H33.9374H33.8636H33.7899H33.7163H33.6428H33.5695H33.4963H33.4232H33.3503H33.2774H33.2047H33.1321H33.0597H32.9873H32.9151H32.843H32.771H32.6992H32.6274H32.5558H32.4843H32.4129H32.3416H32.2704H32.1994H32.1284H32.0576H31.9869H31.9163H31.8458H31.7754H31.7052H31.635H31.5649H31.495H31.4252H31.3554H31.2858H31.2163H31.1469H31.0776H31.0084H30.9393H30.8703H30.8013H30.7325H30.6638H30.5952H30.5267H30.4583H30.39H30.3218H30.2537H30.1857H30.1178H30.0499H29.9822H29.9146H29.847H29.7796H29.7122H29.6449H29.5777H29.5106H29.4436H29.3767H29.3099H29.2431H29.1765H29.1099H29.0434H28.977H28.9107H28.8444H28.7783H28.7122H28.6462H28.5803H28.5145H28.4487H28.383H28.3174H28.2519H28.1865H28.1211H28.0558H27.9906H27.9254H27.8603H27.7953H27.7304H27.6655H27.6008H27.536H27.4714H27.4068H27.3423H27.2778H27.2135H27.1492H27.0849H27.0207H26.9566H26.8925H26.8286H26.7646H26.7008H26.6369H26.5732H26.5095H26.4459H26.3823H26.3188H26.2553H26.1919H26.1286H26.0653H26.0021H25.9389H25.8758H25.8127H25.7497H25.6867H25.6238H25.5609H25.4981H25.4353H25.3726H25.3099H25.2473H25.1847H25.1222H25.0597H24.9972H24.9348H24.8725H24.8102H24.7479H24.6856H24.6234H24.5613H24.4992H24.4371H24.375H24.313H24.2511H24.1891H24.1272H24.0654H24.0036H23.9418H23.88H23.8183H23.7566H23.6949H23.6333H23.5717H23.5101H23.4485H23.387H23.3255H23.264H23.2026H23.1412H23.0798H23.0184H22.9571H22.8957H22.8344H22.7731H22.7119H22.6506H22.5894H22.5282H22.467H22.4058H22.3447H22.2836H22.2224H22.1613H22.1002H22.0392H21.9781H21.917H21.856H21.795H21.7339H21.6729H21.6119H21.5509H21.4899H21.429H21.368H21.307H21.246H21.1851H21.1241H21.0632H21.0022H20.9413H20.8803H20.8194H20.7584H20.6975H20.6365H20.5756H20.5146H20.4537H20.3927H20.3318H20.2708H20.2098H20.1488H20.0878H20.0268H19.9658H19.9048H19.8438H19.7828H19.7217H19.6607H19.5996H19.5385H19.4774H19.4163H19.3552H19.294H19.2329H19.1717H19.1105H19.0493H18.9881H18.9268H18.8656H18.8043H18.743H18.6816H18.6203H18.5589H18.4975H18.4361H18.3746H18.3132H18.2517H18.1901H18.1286H18.067H18.0054H17.9437H17.8821H17.8203H17.7586H17.6968H17.635H17.5732H17.5113H17.4494H17.3875H17.3255H17.2635H17.2015H17.1394H17.0772H17.0151H16.9529H16.8906H16.8283H16.766H16.7036H16.6412H16.5788H16.5163H16.4537H16.3911H16.3285H16.2658H16.2031H16.1403H16.0775H16.0146H15.9517H15.8887H15.8256H15.7626H15.6994H15.6362H15.573H15.5097H15.4463H15.3829H15.3195H15.2559H15.1924H15.1287H15.065H15.0013H14.9374H14.8736H14.8096H14.7456H14.6815H14.6174H14.5532H14.4889H14.4246H14.3602H14.2958H14.2312H14.1666H14.102H14.0373H13.9724H13.9076H13.8426H13.7776H13.7125H13.6474H13.5821H13.5168H13.4514H13.386H13.3204H13.2548H13.1891H13.1233H13.0575H12.9916H12.9255H12.8594H12.7933H12.727H12.6607H12.5943H12.5277H12.4612H12.3945H12.3277H12.2609H12.1939H12.1269H12.0598H11.9926H11.9253H11.8579H11.7904H11.7229H11.6552H11.5874H11.5196H11.4517H11.3836H11.3155H11.2473H11.1789H11.1105H11.042H10.9734H10.9046H10.8358H10.7669H10.6979H10.6288H10.5595H10.4902H10.4208H10.3512H10.2816H10.2118H10.142H10.072H10.0019H9.93175H9.86145H9.79105H9.72054H9.64992H9.57918H9.50834H9.43738H9.3663H9.29511H9.22381H9.15239H9.08085H9.0092H8.93743H8.86554H8.79354H8.72141H8.64916H8.5768H8.50431H8.4317H8.35896H8.28611H8.21312H8.14002H8.06679H7.99343H7.91995H7.84634H7.7726H7.69873H7.62473H7.55061H7.47635H7.40196H7.32744H7.25279H7.17801H7.10309H7.02804H6.95285H6.87753H6.80207H6.72647H6.65074H6.57487H6.49886H6.42271H6.34642H6.26998H6.19341H6.1167H6.03984H5.96284H5.8857H5.80841H5.73098H5.6534H5.57567H5.4978H5.41978H5.34161H5.26329H5.18482H5.1062H5.02743H4.94851H4.86944H4.79021H4.71083H4.6313H4.55161H4.47177H4.39177H4.31161H4.2313H4.15082H4.07019H3.9894H3.90845H3.82734H3.74607H3.66464H3.58304H3.50128H3.41936H3.33727H3.25501H3.1726H3.09001H3.00726H2.92434C2.78171 170.132 2.70206 170.098 2.64924 170.065C2.5881 170.026 2.51071 169.954 2.43479 169.823C2.26892 169.538 2.19357 169.112 2.25253 168.743C3.65626 159.954 11.3802 153.206 20.7252 153.206C30.0703 153.206 37.7943 159.954 39.198 168.743C39.3079 169.431 39.1364 169.686 39.0452 169.781C38.9216 169.91 38.5692 170.132 37.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/>
<circle cx="34.7328" cy="155.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M51.7263 183.132H51.6454H51.5646H51.484H51.4036H51.3233H51.2432H51.1632H51.0834H51.0037H50.9242H50.8449H50.7657H50.6866H50.6077H50.529H50.4504H50.372H50.2937H50.2155H50.1376H50.0597H49.982H49.9045H49.8271H49.7498H49.6727H49.5957H49.5189H49.4422H49.3657H49.2893H49.213H49.1369H49.0609H48.985H48.9093H48.8338H48.7583H48.683H48.6079H48.5329H48.458H48.3832H48.3086H48.2341H48.1597H48.0855H48.0114H47.9374H47.8636H47.7899H47.7163H47.6428H47.5695H47.4963H47.4232H47.3503H47.2774H47.2047H47.1321H47.0597H46.9873H46.9151H46.843H46.771H46.6992H46.6274H46.5558H46.4843H46.4129H46.3416H46.2704H46.1994H46.1284H46.0576H45.9869H45.9163H45.8458H45.7754H45.7052H45.635H45.5649H45.495H45.4252H45.3554H45.2858H45.2163H45.1469H45.0776H45.0084H44.9393H44.8703H44.8013H44.7325H44.6638H44.5952H44.5267H44.4583H44.39H44.3218H44.2537H44.1857H44.1178H44.0499H43.9822H43.9146H43.847H43.7796H43.7122H43.6449H43.5777H43.5106H43.4436H43.3767H43.3099H43.2431H43.1765H43.1099H43.0434H42.977H42.9107H42.8444H42.7783H42.7122H42.6462H42.5803H42.5145H42.4487H42.383H42.3174H42.2519H42.1865H42.1211H42.0558H41.9906H41.9254H41.8603H41.7953H41.7304H41.6655H41.6008H41.536H41.4714H41.4068H41.3423H41.2778H41.2135H41.1492H41.0849H41.0207H40.9566H40.8925H40.8286H40.7646H40.7008H40.6369H40.5732H40.5095H40.4459H40.3823H40.3188H40.2553H40.1919H40.1286H40.0653H40.0021H39.9389H39.8758H39.8127H39.7497H39.6867H39.6238H39.5609H39.4981H39.4353H39.3726H39.3099H39.2473H39.1847H39.1222H39.0597H38.9972H38.9348H38.8725H38.8102H38.7479H38.6856H38.6234H38.5613H38.4992H38.4371H38.375H38.313H38.2511H38.1891H38.1272H38.0654H38.0036H37.9418H37.88H37.8183H37.7566H37.6949H37.6333H37.5717H37.5101H37.4485H37.387H37.3255H37.264H37.2026H37.1412H37.0798H37.0184H36.9571H36.8957H36.8344H36.7731H36.7119H36.6506H36.5894H36.5282H36.467H36.4058H36.3447H36.2836H36.2224H36.1613H36.1002H36.0392H35.9781H35.917H35.856H35.795H35.7339H35.6729H35.6119H35.5509H35.4899H35.429H35.368H35.307H35.246H35.1851H35.1241H35.0632H35.0022H34.9413H34.8803H34.8194H34.7584H34.6975H34.6365H34.5756H34.5146H34.4537H34.3927H34.3318H34.2708H34.2098H34.1488H34.0878H34.0268H33.9658H33.9048H33.8438H33.7828H33.7217H33.6607H33.5996H33.5385H33.4774H33.4163H33.3552H33.294H33.2329H33.1717H33.1105H33.0493H32.9881H32.9268H32.8656H32.8043H32.743H32.6816H32.6203H32.5589H32.4975H32.4361H32.3746H32.3132H32.2517H32.1901H32.1286H32.067H32.0054H31.9437H31.8821H31.8203H31.7586H31.6968H31.635H31.5732H31.5113H31.4494H31.3875H31.3255H31.2635H31.2015H31.1394H31.0772H31.0151H30.9529H30.8906H30.8283H30.766H30.7036H30.6412H30.5788H30.5163H30.4537H30.3911H30.3285H30.2658H30.2031H30.1403H30.0775H30.0146H29.9517H29.8887H29.8256H29.7626H29.6994H29.6362H29.573H29.5097H29.4463H29.3829H29.3195H29.2559H29.1924H29.1287H29.065H29.0013H28.9374H28.8736H28.8096H28.7456H28.6815H28.6174H28.5532H28.4889H28.4246H28.3602H28.2958H28.2312H28.1666H28.102H28.0373H27.9724H27.9076H27.8426H27.7776H27.7125H27.6474H27.5821H27.5168H27.4514H27.386H27.3204H27.2548H27.1891H27.1233H27.0575H26.9916H26.9255H26.8594H26.7933H26.727H26.6607H26.5943H26.5277H26.4612H26.3945H26.3277H26.2609H26.1939H26.1269H26.0598H25.9926H25.9253H25.8579H25.7904H25.7229H25.6552H25.5874H25.5196H25.4517H25.3836H25.3155H25.2473H25.1789H25.1105H25.042H24.9734H24.9046H24.8358H24.7669H24.6979H24.6288H24.5595H24.4902H24.4208H24.3512H24.2816H24.2118H24.142H24.072H24.0019H23.9317H23.8615H23.7911H23.7205H23.6499H23.5792H23.5083H23.4374H23.3663H23.2951H23.2238H23.1524H23.0809H23.0092H22.9374H22.8655H22.7935H22.7214H22.6492H22.5768H22.5043H22.4317H22.359H22.2861H22.2131H22.14H22.0668H21.9934H21.9199H21.8463H21.7726H21.6987H21.6247H21.5506H21.4764H21.402H21.3274H21.2528H21.178H21.1031H21.028H20.9528H20.8775H20.8021H20.7265H20.6507H20.5749H20.4989H20.4227H20.3464H20.27H20.1934H20.1167H20.0398H19.9628H19.8857H19.8084H19.731H19.6534H19.5757H19.4978H19.4198H19.3416H19.2633H19.1848H19.1062H19.0274H18.9485H18.8694H18.7902H18.7108H18.6313H18.5516H18.4718H18.3918H18.3116H18.2313H18.1508H18.0702H17.9894H17.9085H17.8273H17.7461H17.6646H17.583H17.5013H17.4194H17.3373H17.255H17.1726H17.09H17.0073H16.9243C16.7778 183.132 16.6956 183.097 16.642 183.064C16.5807 183.026 16.5047 182.955 16.4306 182.829C16.2682 182.553 16.1944 182.141 16.2521 181.785C17.6523 173.127 25.3653 166.455 34.7252 166.455C44.0852 166.455 51.7982 173.127 53.1984 181.785C53.3068 182.454 53.138 182.695 53.0518 182.784C52.929 182.91 52.5741 183.132 51.7263 183.132Z" fill="white" stroke="#212529" stroke-width="3"/>
</svg>
`;

View File

@@ -1,6 +1,6 @@
<app-header></app-header>
<bit-container>
<bit-container *ngIf="!IsProviderManaged">
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
@@ -256,3 +256,13 @@
</ng-container>
</ng-container>
</bit-container>
<bit-container *ngIf="IsProviderManaged">
<div
class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-24 tw-text-center tw-font-bold"
>
<bit-icon [icon]="manageBillingFromProviderPortal"></bit-icon>
<ng-container slot="description">{{
"manageBillingFromProviderPortalMessage" | i18n
}}</ng-container>
</div>
</bit-container>

View File

@@ -5,7 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PlanType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
@@ -28,6 +28,7 @@ import {
} from "../shared/offboarding-survey.component";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
import { ManageBilling } from "./icons/manage-billing.icon";
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
@Component({
@@ -47,11 +48,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
loading: boolean;
locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
manageBillingFromProviderPortal = ManageBilling;
IsProviderManaged = false;
protected readonly teamsStarter = ProductType.TeamsStarter;
private destroy$ = new Subject<void>();
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
);
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -99,6 +106,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.loading = true;
this.locale = await firstValueFrom(this.i18nService.locale$);
this.userOrg = await this.organizationService.get(this.organizationId);
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
this.IsProviderManaged =
this.userOrg.hasProvider &&
this.userOrg.providerType == ProviderType.Msp &&
enableConsolidatedBilling
? true
: false;
if (this.userOrg.canViewSubscription) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
this.lineItems = this.sub?.subscription?.items;

View File

@@ -42,7 +42,10 @@
: subscription.expirationWithGracePeriod
) | date: "mediumDate"
}}
<div *ngIf="subscription.hasSeparateGracePeriod" class="tw-text-muted">
<div
*ngIf="subscription.hasSeparateGracePeriod && !subscription.isInTrial"
class="tw-text-muted"
>
{{
"selfHostGracePeriodHelp"
| i18n: (subscription.expirationWithGracePeriod | date: "mediumDate")

View File

@@ -8049,5 +8049,8 @@
},
"collectionItemSelect": {
"message": "Select collection item"
},
"manageBillingFromProviderPortalMessage": {
"message": "Manage billing from the Provider Portal"
}
}

View File

@@ -17,7 +17,7 @@ import { ServiceAccountEventLogApiService } from "./service-account-event-log-ap
templateUrl: "./service-accounts-events.component.html",
})
export class ServiceAccountEventsComponent extends BaseEventsComponent implements OnDestroy {
exportFileName = "service-account-events";
exportFileName = "machine-account-events";
private destroy$ = new Subject<void>();
private serviceAccountId: string;

View File

@@ -14,7 +14,7 @@ export class LoggingErrorHandler extends ErrorHandler {
override handleError(error: any): void {
try {
const logService = this.injector.get(LogService, null);
logService.error(error);
logService.error("Unhandled error in angular", error);
} catch {
super.handleError(error);
}

View File

@@ -2,22 +2,17 @@ const originalConsole = console;
declare let console: any;
export function interceptConsole(interceptions: any): object {
export function interceptConsole(): {
log: jest.Mock<any, any>;
warn: jest.Mock<any, any>;
error: jest.Mock<any, any>;
} {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
return interceptions;
return console;
}
export function restoreConsole() {

View File

@@ -58,4 +58,16 @@ export class SelfHostedOrganizationSubscriptionView implements View {
get isExpiredAndOutsideGracePeriod() {
return this.hasExpiration && this.expirationWithGracePeriod < new Date();
}
/**
* In the case of a trial, where there is no grace period, the expirationWithGracePeriod and expirationWithoutGracePeriod will
* be exactly the same. This can be used to hide the grace period note.
*/
get isInTrial() {
return (
this.expirationWithGracePeriod &&
this.expirationWithoutGracePeriod &&
this.expirationWithGracePeriod.getTime() === this.expirationWithoutGracePeriod.getTime()
);
}
}

View File

@@ -1,9 +1,9 @@
import { LogLevelType } from "../enums/log-level-type.enum";
export abstract class LogService {
abstract debug(message: string): void;
abstract info(message: string): void;
abstract warning(message: string): void;
abstract error(message: string): void;
abstract write(level: LogLevelType, message: string): void;
abstract debug(message?: any, ...optionalParams: any[]): void;
abstract info(message?: any, ...optionalParams: any[]): void;
abstract warning(message?: any, ...optionalParams: any[]): void;
abstract error(message?: any, ...optionalParams: any[]): void;
abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void;
}

View File

@@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "../../../spec";
import { ConsoleLogService } from "./console-log.service";
let caughtMessage: any;
describe("ConsoleLogService", () => {
const error = new Error("this is an error");
const obj = { a: 1, b: 2 };
let consoleSpy: {
log: jest.Mock<any, any>;
warn: jest.Mock<any, any>;
error: jest.Mock<any, any>;
};
let logService: ConsoleLogService;
beforeEach(() => {
caughtMessage = {};
interceptConsole(caughtMessage);
consoleSpy = interceptConsole();
logService = new ConsoleLogService(true);
});
@@ -18,41 +23,41 @@ describe("ConsoleLogService", () => {
it("filters messages below the set threshold", () => {
logService = new ConsoleLogService(true, () => true);
logService.debug("debug");
logService.info("info");
logService.warning("warning");
logService.error("error");
logService.debug("debug", error, obj);
logService.info("info", error, obj);
logService.warning("warning", error, obj);
logService.error("error", error, obj);
expect(caughtMessage).toEqual({});
expect(consoleSpy.log).not.toHaveBeenCalled();
expect(consoleSpy.warn).not.toHaveBeenCalled();
expect(consoleSpy.error).not.toHaveBeenCalled();
});
it("only writes debug messages in dev mode", () => {
logService = new ConsoleLogService(false);
logService.debug("debug message");
expect(caughtMessage.log).toBeUndefined();
expect(consoleSpy.log).not.toHaveBeenCalled();
});
it("writes debug/info messages to console.log", () => {
logService.debug("this is a debug message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is a debug message" },
});
logService.debug("this is a debug message", error, obj);
logService.info("this is an info message", error, obj);
logService.info("this is an info message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is an info message" },
});
expect(consoleSpy.log).toHaveBeenCalledTimes(2);
expect(consoleSpy.log).toHaveBeenCalledWith("this is a debug message", error, obj);
expect(consoleSpy.log).toHaveBeenCalledWith("this is an info message", error, obj);
});
it("writes warning messages to console.warn", () => {
logService.warning("this is a warning message");
expect(caughtMessage).toMatchObject({
warn: { 0: "this is a warning message" },
});
logService.warning("this is a warning message", error, obj);
expect(consoleSpy.warn).toHaveBeenCalledWith("this is a warning message", error, obj);
});
it("writes error messages to console.error", () => {
logService.error("this is an error message");
expect(caughtMessage).toMatchObject({
error: { 0: "this is an error message" },
});
logService.error("this is an error message", error, obj);
expect(consoleSpy.error).toHaveBeenCalledWith("this is an error message", error, obj);
});
});

View File

@@ -9,26 +9,26 @@ export class ConsoleLogService implements LogServiceAbstraction {
protected filter: (level: LogLevelType) => boolean = null,
) {}
debug(message: string) {
debug(message?: any, ...optionalParams: any[]) {
if (!this.isDev) {
return;
}
this.write(LogLevelType.Debug, message);
this.write(LogLevelType.Debug, message, ...optionalParams);
}
info(message: string) {
this.write(LogLevelType.Info, message);
info(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Info, message, ...optionalParams);
}
warning(message: string) {
this.write(LogLevelType.Warning, message);
warning(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Warning, message, ...optionalParams);
}
error(message: string) {
this.write(LogLevelType.Error, message);
error(message?: any, ...optionalParams: any[]) {
this.write(LogLevelType.Error, message, ...optionalParams);
}
write(level: LogLevelType, message: string) {
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
@@ -36,19 +36,19 @@ export class ConsoleLogService implements LogServiceAbstraction {
switch (level) {
case LogLevelType.Debug:
// eslint-disable-next-line
console.log(message);
console.log(message, ...optionalParams);
break;
case LogLevelType.Info:
// eslint-disable-next-line
console.log(message);
console.log(message, ...optionalParams);
break;
case LogLevelType.Warning:
// eslint-disable-next-line
console.warn(message);
console.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
// eslint-disable-next-line
console.error(message);
console.error(message, ...optionalParams);
break;
default:
break;

View File

@@ -235,6 +235,11 @@ export function mockMigrationHelper(
helper.setToUser(userId, keyDefinition, value),
);
mockHelper.getAccounts.mockImplementation(() => helper.getAccounts());
mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds());
mockHelper.removeFromGlobal.mockImplementation((keyDefinition) =>
helper.removeFromGlobal(keyDefinition),
);
mockHelper.remove.mockImplementation((key) => helper.remove(key));
mockHelper.type = helper.type;

View File

@@ -175,8 +175,8 @@ export class MigrationHelper {
* Helper method to read known users ids.
*/
async getKnownUserIds(): Promise<string[]> {
if (this.currentVersion < 61) {
return knownAccountUserIdsBuilderPre61(this.storageService);
if (this.currentVersion < 60) {
return knownAccountUserIdsBuilderPre60(this.storageService);
} else {
return knownAccountUserIdsBuilder(this.storageService);
}
@@ -245,7 +245,7 @@ function globalKeyBuilderPre9(): string {
throw Error("No key builder should be used for versions prior to 9.");
}
async function knownAccountUserIdsBuilderPre61(
async function knownAccountUserIdsBuilderPre60(
storageService: AbstractStorageService,
): Promise<string[]> {
return (await storageService.get<string[]>("authenticatedAccounts")) ?? [];

View File

@@ -51,17 +51,13 @@ const rollbackJson = () => {
},
global_account_accounts: {
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
email: "user1",
name: "User 1",
emailVerified: true,
},
user2: {
profile: {
email: "",
emailVerified: false,
},
email: "",
emailVerified: false,
},
},
global_account_activeAccountId: "user1",

View File

@@ -38,8 +38,8 @@ export class KnownAccountsMigrator extends Migrator<59, 60> {
}
async rollback(helper: MigrationHelper): Promise<void> {
// authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back
const accounts = (await helper.getFromGlobal<Record<string, unknown>>(ACCOUNT_ACCOUNTS)) ?? {};
await helper.set("authenticatedAccounts", Object.keys(accounts));
const userIds = (await helper.getKnownUserIds()) ?? [];
await helper.set("authenticatedAccounts", userIds);
await helper.removeFromGlobal(ACCOUNT_ACCOUNTS);
// Active Account Id

View File

@@ -0,0 +1,33 @@
import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core";
import { FocusableElement } from "../shared/focusable-element";
@Directive({
selector: "bitA11yCell",
standalone: true,
providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }],
})
export class A11yCellDirective implements FocusableElement {
@HostBinding("attr.role")
role: "gridcell" | null;
@ContentChild(FocusableElement)
private focusableChild: FocusableElement;
getFocusTarget() {
let focusTarget: HTMLElement;
if (this.focusableChild) {
focusTarget = this.focusableChild.getFocusTarget();
} else {
focusTarget = this.elementRef.nativeElement.querySelector("button, a");
}
if (!focusTarget) {
return this.elementRef.nativeElement;
}
return focusTarget;
}
constructor(private elementRef: ElementRef<HTMLElement>) {}
}

View File

@@ -0,0 +1,145 @@
import {
AfterViewInit,
ContentChildren,
Directive,
HostBinding,
HostListener,
Input,
QueryList,
} from "@angular/core";
import type { A11yCellDirective } from "./a11y-cell.directive";
import { A11yRowDirective } from "./a11y-row.directive";
@Directive({
selector: "bitA11yGrid",
standalone: true,
})
export class A11yGridDirective implements AfterViewInit {
@HostBinding("attr.role")
role = "grid";
@ContentChildren(A11yRowDirective)
rows: QueryList<A11yRowDirective>;
/** The number of pages to navigate on `PageUp` and `PageDown` */
@Input() pageSize = 5;
private grid: A11yCellDirective[][];
/** The row that currently has focus */
private activeRow = 0;
/** The cell that currently has focus */
private activeCol = 0;
@HostListener("keydown", ["$event"])
onKeyDown(event: KeyboardEvent) {
switch (event.code) {
case "ArrowUp":
this.updateCellFocusByDelta(-1, 0);
break;
case "ArrowRight":
this.updateCellFocusByDelta(0, 1);
break;
case "ArrowDown":
this.updateCellFocusByDelta(1, 0);
break;
case "ArrowLeft":
this.updateCellFocusByDelta(0, -1);
break;
case "Home":
this.updateCellFocusByDelta(-this.activeRow, -this.activeCol);
break;
case "End":
this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length);
break;
case "PageUp":
this.updateCellFocusByDelta(-this.pageSize, 0);
break;
case "PageDown":
this.updateCellFocusByDelta(this.pageSize, 0);
break;
default:
return;
}
/** Prevent default scrolling behavior */
event.preventDefault();
}
ngAfterViewInit(): void {
this.initializeGrid();
}
private initializeGrid(): void {
try {
this.grid = this.rows.map((listItem) => {
listItem.role = "row";
return [...listItem.cells];
});
this.grid.flat().forEach((cell) => {
cell.role = "gridcell";
cell.getFocusTarget().tabIndex = -1;
});
this.getActiveCellContent().tabIndex = 0;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Unable to initialize grid");
}
}
/** Get the focusable content of the active cell */
private getActiveCellContent(): HTMLElement {
return this.grid[this.activeRow][this.activeCol].getFocusTarget();
}
/** Move focus via a delta against the currently active gridcell */
private updateCellFocusByDelta(rowDelta: number, colDelta: number) {
const prevActive = this.getActiveCellContent();
this.activeCol += colDelta;
this.activeRow += rowDelta;
// Row upper bound
if (this.activeRow >= this.grid.length) {
this.activeRow = this.grid.length - 1;
}
// Row lower bound
if (this.activeRow < 0) {
this.activeRow = 0;
}
// Column upper bound
if (this.activeCol >= this.grid[this.activeRow].length) {
if (this.activeRow < this.grid.length - 1) {
// Wrap to next row on right arrow
this.activeCol = 0;
this.activeRow += 1;
} else {
this.activeCol = this.grid[this.activeRow].length - 1;
}
}
// Column lower bound
if (this.activeCol < 0) {
if (this.activeRow > 0) {
// Wrap to prev row on left arrow
this.activeRow -= 1;
this.activeCol = this.grid[this.activeRow].length - 1;
} else {
this.activeCol = 0;
}
}
const nextActive = this.getActiveCellContent();
nextActive.tabIndex = 0;
nextActive.focus();
if (nextActive !== prevActive) {
prevActive.tabIndex = -1;
}
}
}

View File

@@ -0,0 +1,31 @@
import {
AfterViewInit,
ContentChildren,
Directive,
HostBinding,
QueryList,
ViewChildren,
} from "@angular/core";
import { A11yCellDirective } from "./a11y-cell.directive";
@Directive({
selector: "bitA11yRow",
standalone: true,
})
export class A11yRowDirective implements AfterViewInit {
@HostBinding("attr.role")
role: "row" | null;
cells: A11yCellDirective[];
@ViewChildren(A11yCellDirective)
private viewCells: QueryList<A11yCellDirective>;
@ContentChildren(A11yCellDirective)
private contentCells: QueryList<A11yCellDirective>;
ngAfterViewInit(): void {
this.cells = [...this.viewCells, ...this.contentCells];
}
}

View File

@@ -1,5 +1,7 @@
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
import { FocusableElement } from "../shared/focusable-element";
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
const styles: Record<BadgeVariant, string[]> = {
@@ -22,8 +24,9 @@ const hoverStyles: Record<BadgeVariant, string[]> = {
@Directive({
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
providers: [{ provide: FocusableElement, useExisting: BadgeDirective }],
})
export class BadgeDirective {
export class BadgeDirective implements FocusableElement {
@HostBinding("class") get classList() {
return [
"tw-inline-block",
@@ -62,6 +65,10 @@ export class BadgeDirective {
*/
@Input() truncate = true;
getFocusTarget() {
return this.el.nativeElement;
}
private hasHoverEffects = false;
constructor(private el: ElementRef<HTMLElement>) {

View File

@@ -1,6 +1,7 @@
import { Component, HostBinding, Input } from "@angular/core";
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
import { FocusableElement } from "../shared/focusable-element";
export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light";
@@ -123,9 +124,12 @@ const sizes: Record<IconButtonSize, string[]> = {
@Component({
selector: "button[bitIconButton]:not(button[bitButton])",
templateUrl: "icon-button.component.html",
providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }],
providers: [
{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent },
{ provide: FocusableElement, useExisting: BitIconButtonComponent },
],
})
export class BitIconButtonComponent implements ButtonLikeAbstraction {
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
@Input("bitIconButton") icon: string;
@Input() buttonType: IconButtonType;
@@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
this.buttonType = value;
}
getFocusTarget() {
return this.elementRef.nativeElement;
}
constructor(private elementRef: ElementRef) {}
}

View File

@@ -16,6 +16,7 @@ export * from "./form-field";
export * from "./icon-button";
export * from "./icon";
export * from "./input";
export * from "./item";
export * from "./layout";
export * from "./link";
export * from "./menu";

View File

@@ -3,12 +3,7 @@ import { take } from "rxjs/operators";
import { Utils } from "@bitwarden/common/platform/misc/utils";
/**
* Interface for implementing focusable components. Used by the AutofocusDirective.
*/
export abstract class FocusableElement {
focus: () => void;
}
import { FocusableElement } from "../shared/focusable-element";
/**
* Directive to focus an element.
@@ -46,7 +41,7 @@ export class AutofocusDirective {
private focus() {
if (this.focusableElement) {
this.focusableElement.focus();
this.focusableElement.getFocusTarget().focus();
} else {
this.el.nativeElement.focus();
}

View File

@@ -0,0 +1 @@
export * from "./item.module";

View File

@@ -0,0 +1,12 @@
import { Component } from "@angular/core";
import { A11yCellDirective } from "../a11y/a11y-cell.directive";
@Component({
selector: "bit-item-action",
standalone: true,
imports: [],
template: `<ng-content></ng-content>`,
providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }],
})
export class ItemActionComponent extends A11yCellDirective {}

View File

@@ -0,0 +1,16 @@
<div class="tw-flex tw-gap-2 tw-items-center">
<ng-content select="[slot=start]"></ng-content>
<div class="tw-flex tw-flex-col tw-items-start tw-text-start tw-w-full [&_p]:tw-mb-0">
<div class="tw-text-main tw-text-base">
<ng-content></ng-content>
</div>
<div class="tw-text-muted tw-text-sm">
<ng-content select="[slot=secondary]"></ng-content>
</div>
</div>
</div>
<div class="tw-flex tw-gap-2 tw-items-center">
<ng-content select="[slot=end]"></ng-content>
</div>

View File

@@ -0,0 +1,15 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component } from "@angular/core";
@Component({
selector: "bit-item-content, [bit-item-content]",
standalone: true,
imports: [CommonModule],
templateUrl: `item-content.component.html`,
host: {
class:
"fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemContentComponent {}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
@Component({
selector: "bit-item-group",
standalone: true,
imports: [],
template: `<ng-content></ng-content>`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: "tw-block",
},
})
export class ItemGroupComponent {}

View File

@@ -0,0 +1,21 @@
<!-- TODO: Colors will be finalized in the extension refresh feature branch -->
<div
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-300/20 tw-text-main tw-border-solid tw-border-b tw-border-0 tw-rounded-lg tw-mb-1.5"
[ngClass]="
focusVisibleWithin()
? 'tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-primary-600 tw-border-transparent'
: 'tw-border-b-secondary-300 [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-border-b-transparent'
"
>
<bit-item-action class="item-main-content tw-block tw-w-full">
<ng-content></ng-content>
</bit-item-action>
<div
#endSlot
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
[hidden]="endSlot.childElementCount === 0"
>
<ng-content select="[slot=end]"></ng-content>
</div>
</div>

View File

@@ -0,0 +1,29 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core";
import { A11yRowDirective } from "../a11y/a11y-row.directive";
import { ItemActionComponent } from "./item-action.component";
@Component({
selector: "bit-item",
standalone: true,
imports: [CommonModule, ItemActionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "item.component.html",
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
})
export class ItemComponent extends A11yRowDirective {
/**
* We have `:focus-within` and `:focus-visible` but no `:focus-visible-within`
*/
protected focusVisibleWithin = signal(false);
@HostListener("focusin", ["$event.target"])
onFocusIn(target: HTMLElement) {
this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible"));
}
@HostListener("focusout")
onFocusOut() {
this.focusVisibleWithin.set(false);
}
}

View File

@@ -0,0 +1,141 @@
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
import * as stories from "./item.stories";
<Meta of={stories} />
```ts
import { ItemModule } from "@bitwarden/components";
```
# Item
`<bit-item>` is a horizontal card that contains one or more interactive actions.
It is a generic container that can be used for either standalone content, an alternative to tables,
or to list nav links.
<Canvas>
<Story of={stories.Default} />
</Canvas>
## Primary Content
The primary content of an item is supplied by `bit-item-content`.
### Content Types
The content can be a button, anchor, or static container.
```html
<bit-item>
<a bit-item-content routerLink="..."> Hi, I am a link. </a>
</bit-item>
<bit-item>
<button bit-item-content (click)="...">And I am a button.</button>
</bit-item>
<bit-item>
<bit-item-content> I'm just static :( </bit-item-content>
</bit-item>
```
<Canvas>
<Story of={stories.ContentTypes} />
</Canvas>
### Content Slots
`bit-item-content` contains the following slots to help position the content:
| Slot | Description |
| ------------------ | --------------------------------------------------- |
| default | primary text or arbitrary content; fan favorite |
| `slot="secondary"` | supporting text; under the default slot |
| `slot="start"` | commonly an icon or avatar; before the default slot |
| `slot="end"` | commonly an icon; after the default slot |
- Note: There is also an `end` slot within `bit-item` itself. Place
[interactive secondary actions](#secondary-actions) there, and place non-interactive content (such
as icons) in `bit-item-content`
```html
<bit-item>
<button bit-item-content type="button">
<bit-avatar slot="start" text="Foo"></bit-avatar>
foo@bitwarden.com
<ng-container slot="secondary">
<div>Bitwarden.com</div>
<div><em>locked</em></div>
</ng-container>
<i slot="end" class="bwi bwi-lock" aria-hidden="true"></i>
</button>
</bit-item>
```
<Canvas>
<Story of={stories.ContentSlots} />
</Canvas>
## Secondary Actions
Secondary interactive actions can be placed in the item through the `"end"` slot, outside of
`bit-item-content`.
Each action must be wrapped by `<bit-item-action>`.
Actions are commonly icon buttons or badge buttons.
```html
<bit-item>
<button bit-item-content>...</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" aria-label="Copy"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="Options"></button>
</bit-item-action>
</ng-container>
</bit-item>
```
## Item Groups
Groups of items can be associated by wrapping them in the `<bit-item-group>`.
<Canvas>
<Story of={stories.MultipleActionList} />
</Canvas>
<Canvas>
<Story of={stories.SingleActionList} />
</Canvas>
### A11y
Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport.
Item groups utilize arrow-based keyboard navigation
([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)).
Use `aria-label` or `aria-labelledby` to give groups an accessible name.
```html
<bit-item-group aria-label="My Items">
<bit-item>...</bit-item>
<bit-item>...</bit-item>
<bit-item>...</bit-item>
</bit-item-group>
```
### Virtual Scrolling
<Canvas>
<Story of={stories.VirtualScrolling} />
</Canvas>

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
import { ItemGroupComponent } from "./item-group.component";
import { ItemComponent } from "./item.component";
@NgModule({
imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent],
exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent],
})
export class ItemModule {}

View File

@@ -0,0 +1,326 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
import { A11yGridDirective } from "../a11y/a11y-grid.directive";
import { AvatarModule } from "../avatar";
import { BadgeModule } from "../badge";
import { IconButtonModule } from "../icon-button";
import { TypographyModule } from "../typography";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
import { ItemGroupComponent } from "./item-group.component";
import { ItemComponent } from "./item.component";
export default {
title: "Component Library/Item",
component: ItemComponent,
decorators: [
moduleMetadata({
imports: [
CommonModule,
ItemGroupComponent,
AvatarModule,
IconButtonModule,
BadgeModule,
TypographyModule,
ItemActionComponent,
ItemContentComponent,
A11yGridDirective,
ScrollingModule,
],
}),
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
],
} as Meta;
type Story = StoryObj<ItemGroupComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
`,
}),
};
export const ContentSlots: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item>
<button bit-item-content type="button">
<bit-avatar
slot="start"
[text]="'Foo'"
></bit-avatar>
foo@bitwarden.com
<ng-container slot="secondary">
<div>Bitwarden.com</div>
<div><em>locked</em></div>
</ng-container>
<i slot="end" class="bwi bwi-lock" aria-hidden="true"></i>
</button>
</bit-item>
`,
}),
};
export const ContentTypes: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item>
<a bit-item-content href="#">
Hi, I am a link.
</a>
</bit-item>
<bit-item>
<button bit-item-content href="#">
And I am a button.
</button>
</bit-item>
<bit-item>
<bit-item-content>
I'm just static :(
</bit-item-content>
</bit-item>
`,
}),
};
export const TextOverflow: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<div class="tw-text-main tw-mb-4">TODO: Fix truncation</div>
<bit-item>
<bit-item-content>
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
</bit-item-content>
</bit-item>
`,
}),
};
export const MultipleActionList: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item-group aria-label="Multiple Action List">
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
`,
}),
};
export const SingleActionList: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item-group aria-label="Single Action List">
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Foobar
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
</bit-item-group>
`,
}),
};
export const VirtualScrolling: Story = {
render: (_args) => ({
props: {
data: Array.from(Array(100000).keys()),
},
template: /*html*/ `
<cdk-virtual-scroll-viewport [itemSize]="46" class="tw-h-[500px]">
<bit-item-group aria-label="Single Action List">
<bit-item *cdkVirtualFor="let item of data">
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
{{ item }}
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
</cdk-virtual-scroll-viewport>
`,
}),
};

View File

@@ -1,7 +1,7 @@
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { FocusableElement } from "../input/autofocus.directive";
import { FocusableElement } from "../shared/focusable-element";
let nextId = 0;
@@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
@Input() disabled: boolean;
@Input() placeholder: string;
focus() {
this.input.nativeElement.focus();
getFocusTarget() {
return this.input.nativeElement;
}
onChange(searchText: string) {

View File

@@ -0,0 +1,8 @@
/**
* Interface for implementing focusable components.
*
* Used by the `AutofocusDirective` and `A11yGridDirective`.
*/
export abstract class FocusableElement {
getFocusTarget: () => HTMLElement;
}

View File

@@ -49,6 +49,6 @@ $card-icons-base: "../../src/billing/images/cards/";
@import "multi-select/scss/bw.theme.scss";
// Workaround for https://bitwarden.atlassian.net/browse/CL-110
#storybook-docs pre.prismjs {
.sbdocs-preview pre.prismjs {
color: white;
}