1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-26 22:33:44 +00:00

fix conflicts

This commit is contained in:
Brad Deibert
2026-01-21 10:03:46 -08:00
34 changed files with 595 additions and 330 deletions

View File

@@ -112,7 +112,7 @@ jobs:
npm_command: dist:bit:selfhost
- artifact_name: selfhosted-DEV
license_type: "commercial"
image_name: web
image_name: web-dev
npm_command: build:bit:selfhost:dev
git_metadata: true
- artifact_name: cloud-QA

View File

@@ -2473,6 +2473,9 @@
"permanentlyDeletedItem": {
"message": "Item permanently deleted"
},
"archivedItemRestored": {
"message": "Archived item restored"
},
"restoreItem": {
"message": "Restore item"
},

View File

@@ -91,10 +91,18 @@ describe("AutofillConfirmationDialogComponent", () => {
jest.resetAllMocks();
});
const findShowAll = (inFx?: ComponentFixture<AutofillConfirmationDialogComponent>) =>
(inFx || fixture).nativeElement.querySelector(
"button.tw-text-sm.tw-font-medium.tw-cursor-pointer",
) as HTMLButtonElement | null;
const findShowAll = (inFx?: ComponentFixture<AutofillConfirmationDialogComponent>) => {
// Find the button by its text content (showAll or showLess)
const buttons = Array.from(
(inFx || fixture).nativeElement.querySelectorAll("button"),
) as HTMLButtonElement[];
return (
buttons.find((btn) => {
const text = btn.textContent?.trim() || "";
return text === "showAll" || text === "showLess";
}) || null
);
};
it("normalizes currentUrl and savedUrls via Utils.getHostname", () => {
expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0));

View File

@@ -20,7 +20,13 @@
{{ "emptyVaultDescription" | i18n }}
</p>
</ng-container>
<a slot="button" bitButton buttonType="secondary" [routerLink]="['/add-cipher']">
<a
slot="button"
bitButton
buttonType="secondary"
[routerLink]="['/add-cipher']"
[queryParams]="{ prefillNameAndURIFromTab: true }"
>
{{ "newLogin" | i18n }}
</a>
</bit-no-items>

View File

@@ -115,15 +115,22 @@ export class TrashListItemsContainerComponent {
}
async restore(cipher: PopupCipherViewLike) {
let toastMessage;
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.restoreWithServer(cipher.id as string, activeUserId);
if (cipher.archivedDate) {
toastMessage = this.i18nService.t("archivedItemRestored");
} else {
toastMessage = this.i18nService.t("restoredItem");
}
await this.router.navigate(["/trash"]);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
message: toastMessage,
});
} catch (e) {
this.logService.error(e);

View File

@@ -15,8 +15,8 @@
"@bitwarden/storage-core": "file:../../../libs/storage-core",
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "13.0.0",
"yargs": "18.0.0"
"uuid": "9.0.1",
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.19.3",
@@ -121,7 +121,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -150,30 +149,6 @@
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -181,19 +156,83 @@
"license": "MIT"
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"license": "ISC",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=20"
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -209,12 +248,6 @@
"node": ">=0.3.1"
}
},
"node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -233,16 +266,12 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"license": "MIT",
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=8"
}
},
"node_modules/make-error": {
@@ -257,36 +286,49 @@
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=0.10.0"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"ansi-regex": "^6.0.1"
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=12"
"node": ">=8"
}
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
"engines": {
"node": ">=8"
}
},
"node_modules/ts-node": {
@@ -337,7 +379,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -353,16 +394,15 @@
"license": "MIT"
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
@@ -371,23 +411,6 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -398,29 +421,28 @@
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"license": "MIT",
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^9.0.1",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
"yargs-parser": "^21.1.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"license": "ISC",
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
"node": ">=12"
}
},
"node_modules/yn": {

View File

@@ -20,8 +20,8 @@
"@bitwarden/logging": "dist/libs/logging/src",
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "13.0.0",
"yargs": "18.0.0"
"uuid": "9.0.1",
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.19.3",
@@ -31,6 +31,12 @@
"@bitwarden/common": "dist/libs/common/src",
"@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service",
"@bitwarden/storage-core": "dist/libs/storage-core/src",
"@bitwarden/logging": "dist/libs/logging/src"
"@bitwarden/logging": "dist/libs/logging/src",
"@bitwarden/client-type": "dist/libs/client-type/src",
"@bitwarden/state": "dist/libs/state/src",
"@bitwarden/state-internal": "dist/libs/state-internal/src",
"@bitwarden/messaging": "dist/libs/messaging/src",
"@bitwarden/guid": "dist/libs/guid/src",
"@bitwarden/serialization": "dist/libs/serialization/src"
}
}

View File

@@ -11,6 +11,7 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { CredentialCreatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("name", {
@@ -25,6 +26,10 @@ const { name } = argv;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
@@ -42,7 +47,10 @@ const { name } = argv;
// Get active account userId
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
const activeUser = status.payload.filter(
(a: { active: boolean; status: string; id: string }) =>
a.active === true && a.status === "unlocked",
)[0];
if (activeUser === undefined) {
LogUtils.logError("No active or unlocked user");
}

View File

@@ -7,6 +7,7 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("uri", {
@@ -21,6 +22,10 @@ const { uri } = argv;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");

View File

@@ -11,6 +11,7 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { CredentialUpdatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
// Command line arguments
@@ -49,6 +50,10 @@ const { name, username, password, uri, credentialId } = argv;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
@@ -67,7 +72,10 @@ const { name, username, password, uri, credentialId } = argv;
// Get active account userId
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
const activeUser = status.payload.filter(
(a: { active: boolean; status: string; id: string }) =>
a.active === true && a.status === "unlocked",
)[0];
if (activeUser === undefined) {
LogUtils.logError("No active or unlocked user");
}

View File

@@ -7,6 +7,7 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("userId", {
@@ -21,6 +22,10 @@ const { userId } = argv;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");

View File

@@ -4,11 +4,16 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
const response = await nativeMessageService.sendHandshake(

View File

@@ -4,11 +4,16 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { LogUtils } from "../log-utils";
import NativeMessageService from "../native-message.service";
import { TestRunnerSdkLoadService } from "../sdk-load.service";
import * as config from "../variables";
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Initialize SDK before using crypto functions
const sdkLoadService = new TestRunnerSdkLoadService();
await sdkLoadService.loadAndInit();
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
LogUtils.logInfo("Sending Handshake");

View File

@@ -4,8 +4,8 @@
// while allowing an unrelated event to fulfill it elsewhere.
export default class Deferred<T> {
private promise: Promise<T>;
private resolver: (T?) => void;
private rejecter: (Error?) => void;
private resolver!: (value?: T) => void;
private rejecter!: (reason?: Error) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {

View File

@@ -13,7 +13,7 @@ import { race } from "./race";
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
export type MessageHandler = (MessageCommon) => void;
export type MessageHandler = (message: MessageCommon) => void;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums

View File

@@ -8,8 +8,8 @@ export const race = <T>({
promise: Promise<T>;
timeout: number;
error?: Error;
}) => {
let timer = null;
}): Promise<T> => {
let timer: NodeJS.Timeout | null = null;
// Similar to Promise.all, but instead of waiting for all, it resolves once one promise finishes.
// Using this so we can reject if the timeout threshold is hit
@@ -20,7 +20,9 @@ export const race = <T>({
}),
promise.then((value) => {
clearTimeout(timer);
if (timer != null) {
clearTimeout(timer);
}
return value;
}),
]);

View File

@@ -0,0 +1,22 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { LogUtils } from "./log-utils";
/**
* SDK Load Service for the native messaging test runner.
* For Node.js environments, the SDK's Node.js build automatically loads WASM from the filesystem.
* No additional initialization is needed.
*/
export class TestRunnerSdkLoadService extends SdkLoadService {
async load(): Promise<void> {
// In Node.js, @bitwarden/sdk-internal automatically loads the WASM file
// from node/bitwarden_wasm_internal_bg.wasm using fs.readFileSync.
// No explicit loading is required.
}
override async loadAndInit(): Promise<void> {
LogUtils.logInfo("Initializing SDK");
await super.loadAndInit();
LogUtils.logSuccess("SDK initialized");
}
}

View File

@@ -1,4 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "dist",
@@ -18,7 +19,13 @@
"@bitwarden/auth/*": ["../../../libs/auth/src/*"],
"@bitwarden/common/*": ["../../../libs/common/src/*"],
"@bitwarden/key-management": ["../../../libs/key-management/src/"],
"@bitwarden/node/*": ["../../../libs/node/src/*"]
"@bitwarden/node/*": ["../../../libs/node/src/*"],
"@bitwarden/state": ["../../../libs/state/src/index.ts"],
"@bitwarden/state-internal": ["../../../libs/state-internal/src/index.ts"],
"@bitwarden/client-type": ["../../../libs/client-type/src/index.ts"],
"@bitwarden/messaging": ["../../../libs/messaging/src/index.ts"],
"@bitwarden/guid": ["../../../libs/guid/src/index.ts"],
"@bitwarden/serialization": ["../../../libs/serialization/src/index.ts"]
},
"plugins": [
{
@@ -26,5 +33,6 @@
}
]
},
"exclude": ["node_modules"]
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -2092,6 +2092,9 @@
"permanentlyDeletedItem": {
"message": "Item permanently deleted"
},
"archivedItemRestored": {
"message": "Archived item restored"
},
"restoredItem": {
"message": "Item restored"
},

View File

@@ -173,16 +173,23 @@ export class ItemFooterComponent implements OnInit, OnChanges {
}
async restore(): Promise<boolean> {
let toastMessage;
if (!this.cipher.isDeleted) {
return false;
}
if (this.cipher.isArchived) {
toastMessage = this.i18nService.t("archivedItemRestored");
} else {
toastMessage = this.i18nService.t("restoredItem");
}
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.restoreCipher(activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("restoredItem"),
message: toastMessage,
});
this.onRestore.emit(this.cipher);
} catch (e) {

View File

@@ -35,89 +35,80 @@
}
</bit-toggle-group>
}
<bit-table [dataSource]="dataSource">
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
@if (!isAdminConsoleActive) {
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
</tr>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
</ng-container>
}
<tbody>
<ng-template body let-rows$>
<!-- prettier-ignore -->
@for (r of (rows$ | async); track r) {
<tr bitRow>
<td bitCell>
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
@if (!organization || canManageCipher(r)) {
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(r)"
title="{{ 'editItemWithName' | i18n: r.name }}"
>{{ r.name }}</a
>
} @else {
<span>{{ r.name }}</span>
}
@if (!organization && r.organizationId) {
<i
class="bwi bwi-collection-shared tw-ml-1"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
}
@if (r.hasAttachments) {
<i
class="bwi bwi-paperclip tw-ml-1"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
}
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
@if (!organization) {
<app-org-badge
[disabled]="disabled"
[organizationId]="r.organizationId"
[organizationName]="
r.organizationId | orgNameFromId: (organizations$ | async)
"
appStopProp
>
</app-org-badge>
}
</td>
<td bitCell class="tw-text-right">
@if (cipherDocs.has(r.id)) {
<a
bitBadge
href="{{ cipherDocs.get(r.id) }}"
target="_blank"
rel="noreferrer"
>
{{ "instructions" | i18n }}</a
>
}
</td>
</tr>
<ng-template bitRowDef let-row>
<td bitCell>
<app-vault-icon [cipher]="row"></app-vault-icon>
</td>
<td bitCell>
@if (!organization || canManageCipher(row)) {
<ng-container>
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(row)"
title="{{ 'editItemWithName' | i18n: row.name }}"
>{{ row.name }}</a
>
</ng-container>
} @else {
<ng-template>
<span>{{ row.name }}</span>
</ng-template>
}
</ng-template>
</tbody></bit-table
>
@if (!organization && row.organizationId) {
<ng-container>
<i
class="bwi bwi-collection-shared tw-ml-1"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
</ng-container>
}
@if (row.hasAttachments) {
<ng-container>
<i
class="bwi bwi-paperclip tw-ml-1"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
}
<br />
<small>{{ row.subTitle }}</small>
</td>
<td bitCell>
@if (!organization) {
<app-org-badge
[disabled]="disabled"
[organizationId]="row.organizationId"
[organizationName]="row.organizationId | orgNameFromId: (organizations$ | async)"
appStopProp
/>
}
</td>
<td bitCell class="tw-text-right">
@if (cipherDocs.has(row.id)) {
<a bitBadge href="{{ cipherDocs.get(row.id) }}" target="_blank" rel="noreferrer">
{{ "instructions" | i18n }}</a
>
}
</td>
</ng-template>
</bit-table-scroll>
}
</div>
}

View File

@@ -35,75 +35,65 @@
}
</bit-toggle-group>
}
<bit-table [dataSource]="dataSource">
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
@if (!isAdminConsoleActive) {
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
</tr>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
</ng-container>
}
<ng-template body let-rows$>
<!-- prettier-ignore -->
@for (r of (rows$ | async); track r) {
<tr bitRow>
<td bitCell>
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
@if (!organization || canManageCipher(r)) {
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(r)"
title="{{ 'editItemWithName' | i18n: r.name }}"
>{{ r.name }}</a
>
} @else {
<span>{{ r.name }}</span>
}
@if (!organization && r.organizationId) {
<i
class="bwi bwi-collection-shared tw-ml-1"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
}
@if (r.hasAttachments) {
<i
class="bwi bwi-paperclip tw-ml-1"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
}
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
@if (!organization) {
<app-org-badge
[disabled]="disabled"
[organizationId]="r.organizationId"
[organizationName]="
r.organizationId | orgNameFromId: (organizations$ | async)
"
appStopProp
>
</app-org-badge>
}
</td>
</tr>
}
<ng-template bitRowDef let-row>
<td bitCell>
<app-vault-icon [cipher]="row"></app-vault-icon>
</td>
<td bitCell>
@if (!organization || canManageCipher(row)) {
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(row)"
title="{{ 'editItemWithName' | i18n: row.name }}"
>{{ row.name }}</a
>
} @else {
<span>{{ row.name }}</span>
}
@if (!organization && row.organizationId) {
<i
class="bwi bwi-collection-shared tw-ml-1"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
}
@if (row.hasAttachments) {
<i
class="bwi bwi-paperclip tw-ml-1"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
}
<br />
<small>{{ row.subTitle }}</small>
</td>
<td bitCell>
@if (!organization) {
<app-org-badge
[disabled]="disabled"
[organizationId]="row.organizationId"
[organizationName]="row.organizationId | orgNameFromId: (organizations$ | async)"
appStopProp
>
</app-org-badge>
}
</td>
</ng-template>
</bit-table>
</bit-table-scroll>
}
</div>
}

View File

@@ -1,6 +1,6 @@
import { Component, OnDestroy } from "@angular/core";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
@@ -10,20 +10,20 @@ import { filter } from "rxjs/operators";
templateUrl: "reports-layout.component.html",
standalone: false,
})
export class ReportsLayoutComponent implements OnDestroy {
export class ReportsLayoutComponent {
homepage = true;
subscription: Subscription;
constructor(router: Router) {
this.subscription = router.events
.pipe(filter((event) => event instanceof NavigationEnd))
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
const reportsHomeRoute = "/reports";
this.homepage = router.url === reportsHomeRoute;
router.events
.pipe(
takeUntilDestroyed(),
filter((event) => event instanceof NavigationEnd),
)
.subscribe((event) => {
this.homepage = (event as NavigationEnd).url == "/reports";
this.homepage = (event as NavigationEnd).url == reportsHomeRoute;
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}

View File

@@ -86,7 +86,7 @@
@if (showActionButtons) {
<div class="tw-ml-auto">
@if (userCanArchive$ | async) {
@if ((userCanArchive$ | async) && !params.isAdminConsoleAction) {
@if (isCipherArchived) {
<button
type="button"

View File

@@ -249,6 +249,15 @@ describe("VaultItemDialogComponent", () => {
});
describe("archive button", () => {
it("should not show archive button in admin console", () => {
(component as any).userCanArchive$ = of(true);
component.setTestCipher({ canBeArchived: true });
component.setTestParams({ mode: "form", isAdminConsoleAction: true });
fixture.detectChanges();
const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']"));
expect(archiveButton).toBeFalsy();
});
it("should show archive button when the user can archive the item and the item can be archived", () => {
component.setTestCipher({ canBeArchived: true });
(component as any).userCanArchive$ = of(true);

View File

@@ -1271,6 +1271,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
}
restore = async (c: C): Promise<boolean> => {
let toastMessage;
if (!CipherViewLikeUtils.isDeleted(c)) {
return;
}
@@ -1284,13 +1285,19 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
return;
}
if (CipherViewLikeUtils.isArchived(c)) {
toastMessage = this.i18nService.t("archivedItemRestored");
} else {
toastMessage = this.i18nService.t("restoredItem");
}
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
message: toastMessage,
});
this.refresh();
} catch (e) {
@@ -1299,11 +1306,18 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
};
async bulkRestore(ciphers: C[]) {
let toastMessage;
if (ciphers.some((c) => !c.edit)) {
this.showMissingPermissionsError();
return;
}
if (ciphers.some((c) => !CipherViewLikeUtils.isArchived(c))) {
toastMessage = this.i18nService.t("restoredItems");
} else {
toastMessage = this.i18nService.t("archivedItemsRestored");
}
if (!(await this.repromptCipher(ciphers))) {
return;
}
@@ -1323,7 +1337,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItems"),
message: toastMessage,
});
this.refresh();
}

View File

@@ -5418,6 +5418,12 @@
"restoreSelected": {
"message": "Restore selected"
},
"archivedItemRestored": {
"message": "Archived item restored"
},
"archivedItemsRestored": {
"message": "Archived items restored"
},
"restoredItem": {
"message": "Item restored"
},

View File

@@ -23,6 +23,7 @@ export enum FeatureFlag {
MacOsNativeCredentialSync = "macos-native-credential-sync",
WindowsDesktopAutotype = "windows-desktop-autotype",
WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga",
SSHAgentV2 = "ssh-agent-v2",
/* Billing */
TrialPaymentOptional = "PM-8163-trial-payment",
@@ -107,6 +108,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.WindowsDesktopAutotype]: FALSE,
[FeatureFlag.WindowsDesktopAutotypeGA]: FALSE,
[FeatureFlag.SSHAgentV2]: FALSE,
/* Tools */
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,

View File

@@ -2,7 +2,6 @@
@if (breadcrumb.route(); as route) {
<a
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
@@ -14,7 +13,6 @@
<button
type="button"
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
(click)="breadcrumb.onClick($event)"
>
@@ -42,7 +40,6 @@
@if (breadcrumb.route(); as route) {
<a
bitMenuItem
linkType="primary"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
@@ -50,7 +47,7 @@
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</a>
} @else {
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
<button type="button" bitMenuItem (click)="breadcrumb.onClick($event)">
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</button>
}
@@ -61,7 +58,6 @@
@if (breadcrumb.route(); as route) {
<a
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
@@ -73,7 +69,6 @@
<button
type="button"
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
(click)="breadcrumb.onClick($event)"
>

View File

@@ -3,21 +3,34 @@ import { input, HostBinding, Directive, inject, ElementRef, booleanAttribute } f
import { AriaDisableDirective } from "../a11y";
import { ariaDisableElement } from "../utils";
export type LinkType = "primary" | "secondary" | "contrast" | "light";
export const LinkTypes = [
"primary",
"secondary",
"contrast",
"light",
"default",
"subtle",
"success",
"warning",
"danger",
] as const;
export type LinkType = (typeof LinkTypes)[number];
const linkStyles: Record<LinkType, string[]> = {
primary: [
"!tw-text-primary-600",
"hover:!tw-text-primary-700",
"focus-visible:before:tw-ring-primary-600",
],
secondary: ["!tw-text-main", "hover:!tw-text-main", "focus-visible:before:tw-ring-primary-600"],
primary: ["tw-text-fg-brand", "hover:tw-text-fg-brand-strong"],
default: ["tw-text-fg-brand", "hover:tw-text-fg-brand-strong"],
secondary: ["tw-text-fg-heading", "hover:tw-text-fg-heading"],
light: ["tw-text-fg-white", "hover:tw-text-fg-white", "focus-visible:before:tw-ring-fg-contrast"],
subtle: ["!tw-text-fg-heading", "hover:tw-text-fg-heading"],
success: ["tw-text-fg-success", "hover:tw-text-fg-success-strong"],
warning: ["tw-text-fg-warning", "hover:tw-text-fg-warning-strong"],
danger: ["tw-text-fg-danger", "hover:tw-text-fg-danger-strong"],
contrast: [
"!tw-text-contrast",
"hover:!tw-text-contrast",
"focus-visible:before:tw-ring-text-contrast",
"tw-text-fg-contrast",
"hover:tw-text-fg-contrast",
"focus-visible:before:tw-ring-fg-contrast",
],
light: ["!tw-text-alt2", "hover:!tw-text-alt2", "focus-visible:before:tw-ring-text-alt2"],
};
const commonStyles = [
@@ -32,16 +45,18 @@ const commonStyles = [
"tw-rounded",
"tw-transition",
"tw-no-underline",
"tw-cursor-pointer",
"hover:tw-underline",
"hover:tw-decoration-1",
"disabled:tw-no-underline",
"disabled:tw-cursor-not-allowed",
"disabled:!tw-text-secondary-300",
"disabled:hover:!tw-text-secondary-300",
"disabled:!tw-text-fg-disabled",
"disabled:hover:!tw-text-fg-disabled",
"disabled:hover:tw-no-underline",
"focus-visible:tw-outline-none",
"focus-visible:tw-underline",
"focus-visible:tw-decoration-1",
"focus-visible:before:tw-ring-border-focus",
// Workaround for html button tag not being able to be set to `display: inline`
// and at the same time not being able to use `tw-ring-offset` because of box-shadow issue.
@@ -63,14 +78,14 @@ const commonStyles = [
"focus-visible:tw-z-10",
"aria-disabled:tw-no-underline",
"aria-disabled:tw-pointer-events-none",
"aria-disabled:!tw-text-secondary-300",
"aria-disabled:hover:!tw-text-secondary-300",
"aria-disabled:!tw-text-fg-disabled",
"aria-disabled:hover:!tw-text-fg-disabled",
"aria-disabled:hover:tw-no-underline",
];
@Directive()
abstract class LinkDirective {
readonly linkType = input<LinkType>("primary");
readonly linkType = input<LinkType>("default");
}
/**

View File

@@ -18,10 +18,15 @@ import { LinkModule } from "@bitwarden/components";
You can use one of the following variants by providing it as the `linkType` input:
- `primary` - most common, uses brand color
- `secondary` - matches the main text color
- @deprecated `primary` => use `default` instead
- @deprecated `secondary` => use `subtle` instead
- `default` - most common, uses brand color
- `subtle` - matches the main text color
- `contrast` - for high contrast against a dark background (or a light background in dark mode)
- `light` - always a light color, even in dark mode
- `warning` - used in association with warning callouts/banners
- `success` - used in association with success callouts/banners
- `danger` - used in association with danger callouts/banners
## Sizes

View File

@@ -2,7 +2,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet";
import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive";
import { AnchorLinkDirective, ButtonLinkDirective, LinkTypes } from "./link.directive";
import { LinkModule } from "./link.module";
export default {
@@ -14,7 +14,7 @@ export default {
],
argTypes: {
linkType: {
options: ["primary", "secondary", "contrast"],
options: LinkTypes.map((type) => type),
control: { type: "radio" },
},
},
@@ -30,48 +30,153 @@ type Story = StoryObj<ButtonLinkDirective>;
export const Default: Story = {
render: (args) => ({
props: {
linkType: args.linkType,
backgroundClass:
args.linkType === "contrast"
? "tw-bg-bg-contrast"
: args.linkType === "light"
? "tw-bg-bg-brand"
: "tw-bg-transparent",
},
template: /*html*/ `
<a bitLink ${formatArgsForCodeSnippet<ButtonLinkDirective>(args)}>Your text here</a>
<div class="tw-p-2" [class]="backgroundClass">
<a bitLink href="" ${formatArgsForCodeSnippet<ButtonLinkDirective>(args)}>Your text here</a>
</div>
`,
}),
args: {
linkType: "primary",
},
parameters: {
chromatic: { disableSnapshot: true },
},
};
export const AllVariations: Story = {
render: () => ({
template: /*html*/ `
<div class="tw-flex tw-flex-col tw-gap-6">
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="primary" href="#">Primary</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="secondary" href="#">Secondary</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2 tw-bg-bg-contrast">
<a bitLink linkType="contrast" href="#">Contrast</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2 tw-bg-bg-brand">
<a bitLink linkType="light" href="#">Light</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="default" href="#">Default</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="subtle" href="#">Subtle</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="success" href="#">Success</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="warning" href="#">Warning</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="danger" href="#">Danger</a>
</div>
</div>
`,
}),
parameters: {
controls: {
exclude: ["linkType"],
hideNoControlsWarning: true,
},
},
};
export const InteractionStates: Story = {
render: () => ({
template: /*html*/ `
<div class="tw-flex tw-gap-4 tw-p-2 tw-mb-6">
<div class="tw-flex tw-flex-col tw-gap-6">
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="primary" href="#">Primary</a>
<a bitLink linkType="primary" href="#" class="tw-test-hover">Primary</a>
<a bitLink linkType="primary" href="#" class="tw-test-focus-visible">Primary</a>
<a bitLink linkType="primary" href="#" class="tw-test-hover tw-test-focus-visible">Primary</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2 tw-mb-6">
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="secondary" href="#">Secondary</a>
<a bitLink linkType="secondary" href="#" class="tw-test-hover">Secondary</a>
<a bitLink linkType="secondary" href="#" class="tw-test-focus-visible">Secondary</a>
<a bitLink linkType="secondary" href="#" class="tw-test-hover tw-test-focus-visible">Secondary</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2 tw-mb-6 tw-bg-primary-600">
<div class="tw-flex tw-gap-4 tw-p-2 tw-bg-bg-contrast">
<a bitLink linkType="contrast" href="#">Contrast</a>
<a bitLink linkType="contrast" href="#" class="tw-test-hover">Contrast</a>
<a bitLink linkType="contrast" href="#" class="tw-test-focus-visible">Contrast</a>
<a bitLink linkType="contrast" href="#" class="tw-test-hover tw-test-focus-visible">Contrast</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2 tw-mb-6 tw-bg-primary-600">
<div class="tw-flex tw-gap-4 tw-p-2 tw-bg-bg-brand">
<a bitLink linkType="light" href="#">Light</a>
<a bitLink linkType="light" href="#" class="tw-test-hover">Light</a>
<a bitLink linkType="light" href="#" class="tw-test-focus-visible">Light</a>
<a bitLink linkType="light" href="#" class="tw-test-hover tw-test-focus-visible">Light</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="default" href="#">Default</a>
<a bitLink linkType="default" href="#" class="tw-test-hover">Default</a>
<a bitLink linkType="default" href="#" class="tw-test-focus-visible">Default</a>
<a bitLink linkType="default" href="#" class="tw-test-hover tw-test-focus-visible">Default</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="subtle" href="#">Subtle</a>
<a bitLink linkType="subtle" href="#" class="tw-test-hover">Subtle</a>
<a bitLink linkType="subtle" href="#" class="tw-test-focus-visible">Subtle</a>
<a bitLink linkType="subtle" href="#" class="tw-test-hover tw-test-focus-visible">Subtle</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="success" href="#">Success</a>
<a bitLink linkType="success" href="#" class="tw-test-hover">Success</a>
<a bitLink linkType="success" href="#" class="tw-test-focus-visible">Success</a>
<a bitLink linkType="success" href="#" class="tw-test-hover tw-test-focus-visible">Success</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="warning" href="#">Warning</a>
<a bitLink linkType="warning" href="#" class="tw-test-hover">Warning</a>
<a bitLink linkType="warning" href="#" class="tw-test-focus-visible">Warning</a>
<a bitLink linkType="warning" href="#" class="tw-test-hover tw-test-focus-visible">Warning</a>
</div>
<div class="tw-flex tw-gap-4 tw-p-2">
<a bitLink linkType="danger" href="#">Danger</a>
<a bitLink linkType="danger" href="#" class="tw-test-hover">Danger</a>
<a bitLink linkType="danger" href="#" class="tw-test-focus-visible">Danger</a>
<a bitLink linkType="danger" href="#" class="tw-test-hover tw-test-focus-visible">Danger</a>
</div>
</div>
`,
}),
parameters: {
controls: {
exclude: ["linkType"],
hideNoControlsWarning: true,
},
},
};
export const Buttons: Story = {
render: (args) => ({
props: args,
props: {
linkType: args.linkType,
backgroundClass:
args.linkType === "contrast"
? "tw-bg-bg-contrast"
: args.linkType === "light"
? "tw-bg-bg-brand"
: "tw-bg-transparent",
},
template: /*html*/ `
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-600': linkType === 'contrast' }">
<div class="tw-p-2" [class]="backgroundClass">
<div class="tw-block tw-p-2">
<button type="button" bitLink [linkType]="linkType">Button</button>
</div>
@@ -100,9 +205,17 @@ export const Buttons: Story = {
export const Anchors: StoryObj<AnchorLinkDirective> = {
render: (args) => ({
props: args,
props: {
linkType: args.linkType,
backgroundClass:
args.linkType === "contrast"
? "tw-bg-bg-contrast"
: args.linkType === "light"
? "tw-bg-bg-brand"
: "tw-bg-transparent",
},
template: /*html*/ `
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-600': linkType === 'contrast' }">
<div class="tw-p-2" [class]="backgroundClass">
<div class="tw-block tw-p-2">
<a bitLink [linkType]="linkType" href="#">Anchor</a>
</div>
@@ -138,18 +251,15 @@ export const Inline: Story = {
</span>
`,
}),
args: {
linkType: "primary",
},
};
export const Disabled: Story = {
export const Inactive: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" bitLink disabled linkType="primary" class="tw-me-2">Primary</button>
<button type="button" bitLink disabled linkType="secondary" class="tw-me-2">Secondary</button>
<div class="tw-bg-primary-600 tw-p-2 tw-inline-block">
<div class="tw-bg-bg-contrast tw-p-2 tw-inline-block">
<button type="button" bitLink disabled linkType="contrast">Contrast</button>
</div>
`,

View File

@@ -73,7 +73,6 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
A random password
<button
bitLink
linkType="primary"
[bitPopoverTriggerFor]="myPopover"
#triggerRef="popoverTrigger"
type="button"

View File

@@ -112,13 +112,12 @@ class KitchenSinkDialogComponent {
<div class="tw-my-6">
<h1 bitTypography="h1">Bitwarden Kitchen Sink<bit-avatar text="Bit Warden"></bit-avatar></h1>
<a bitLink linkType="primary" href="#">This is a link</a>
<a bitLink href="#">This is a link</a>
<p bitTypography="body1" class="tw-inline">
&nbsp;and this is a link button popover trigger:&nbsp;
</p>
<button
bitLink
linkType="primary"
[bitPopoverTriggerFor]="myPopover"
#triggerRef="popoverTrigger"
type="button"