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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
"clipboardWrite",
|
||||
"idle",
|
||||
"scripting",
|
||||
"offscreen"
|
||||
"offscreen",
|
||||
"webRequest",
|
||||
"webRequestAuthProvider"
|
||||
],
|
||||
"optional_permissions": ["nativeMessaging", "privacy"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -8049,5 +8049,8 @@
|
||||
},
|
||||
"collectionItemSelect": {
|
||||
"message": "Select collection item"
|
||||
},
|
||||
"manageBillingFromProviderPortalMessage": {
|
||||
"message": "Manage billing from the Provider Portal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")) ?? [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
33
libs/components/src/a11y/a11y-cell.directive.ts
Normal file
33
libs/components/src/a11y/a11y-cell.directive.ts
Normal 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>) {}
|
||||
}
|
||||
145
libs/components/src/a11y/a11y-grid.directive.ts
Normal file
145
libs/components/src/a11y/a11y-grid.directive.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
libs/components/src/a11y/a11y-row.directive.ts
Normal file
31
libs/components/src/a11y/a11y-row.directive.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
1
libs/components/src/item/index.ts
Normal file
1
libs/components/src/item/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./item.module";
|
||||
12
libs/components/src/item/item-action.component.ts
Normal file
12
libs/components/src/item/item-action.component.ts
Normal 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 {}
|
||||
16
libs/components/src/item/item-content.component.html
Normal file
16
libs/components/src/item/item-content.component.html
Normal 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>
|
||||
15
libs/components/src/item/item-content.component.ts
Normal file
15
libs/components/src/item/item-content.component.ts
Normal 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 {}
|
||||
13
libs/components/src/item/item-group.component.ts
Normal file
13
libs/components/src/item/item-group.component.ts
Normal 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 {}
|
||||
21
libs/components/src/item/item.component.html
Normal file
21
libs/components/src/item/item.component.html
Normal 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>
|
||||
29
libs/components/src/item/item.component.ts
Normal file
29
libs/components/src/item/item.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
141
libs/components/src/item/item.mdx
Normal file
141
libs/components/src/item/item.mdx
Normal 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>
|
||||
12
libs/components/src/item/item.module.ts
Normal file
12
libs/components/src/item/item.module.ts
Normal 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 {}
|
||||
326
libs/components/src/item/item.stories.ts
Normal file
326
libs/components/src/item/item.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
8
libs/components/src/shared/focusable-element.ts
Normal file
8
libs/components/src/shared/focusable-element.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Interface for implementing focusable components.
|
||||
*
|
||||
* Used by the `AutofocusDirective` and `A11yGridDirective`.
|
||||
*/
|
||||
export abstract class FocusableElement {
|
||||
getFocusTarget: () => HTMLElement;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user