mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00:00
[PM-5189] Merging main and fixing conflicts
This commit is contained in:
2
.github/workflows/deploy-web.yml
vendored
2
.github/workflows/deploy-web.yml
vendored
@@ -224,7 +224,7 @@ jobs:
|
|||||||
project: Clients
|
project: Clients
|
||||||
environment: ${{ needs.setup.outputs.environment-name }}
|
environment: ${{ needs.setup.outputs.environment-name }}
|
||||||
tag: ${{ inputs.branch-or-tag }}
|
tag: ${{ inputs.branch-or-tag }}
|
||||||
slack-channel: team-eng-qa-devops
|
slack-channel: alerts-deploy-qa
|
||||||
event: 'start'
|
event: 'start'
|
||||||
commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }}
|
commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }}
|
||||||
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
||||||
|
|||||||
32
.github/workflows/version-bump.yml
vendored
32
.github/workflows/version-bump.yml
vendored
@@ -452,6 +452,38 @@ jobs:
|
|||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
- name: Report upcoming browser release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} && ${{ steps.set-final-version-output.outputs.version_browser != '' }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version_browser }}
|
||||||
|
project: browser
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Report upcoming cli release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} && ${{ steps.set-final-version-output.outputs.version_cli != '' }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version_cli }}
|
||||||
|
project: cli
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Report upcoming desktop release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} && ${{ steps.set-final-version-output.outputs.version_desktop != '' }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version_desktop }}
|
||||||
|
project: desktop
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Report upcoming web release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} && ${{ steps.set-final-version-output.outputs.version_web != '' }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version_web }}
|
||||||
|
project: web
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
cut_rc:
|
cut_rc:
|
||||||
name: Cut RC branch
|
name: Cut RC branch
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
|||||||
@@ -3137,6 +3137,41 @@
|
|||||||
"message": "to make them visible.",
|
"message": "to make them visible.",
|
||||||
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
|
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
|
||||||
},
|
},
|
||||||
|
"autofillSuggestions": {
|
||||||
|
"message": "Auto-fill suggestions"
|
||||||
|
},
|
||||||
|
"autofillSuggestionsTip": {
|
||||||
|
"message": "Save a login item for this site to auto-fill"
|
||||||
|
},
|
||||||
|
"yourVaultIsEmpty": {
|
||||||
|
"message": "Your vault is empty"
|
||||||
|
},
|
||||||
|
"noItemsMatchSearch": {
|
||||||
|
"message": "No items match your search"
|
||||||
|
},
|
||||||
|
"clearFiltersOrTryAnother": {
|
||||||
|
"message": "Clear filters or try another search term"
|
||||||
|
},
|
||||||
|
"copyInfo": {
|
||||||
|
"message": "Copy info, $ITEMNAME$",
|
||||||
|
"description": "Aria label for a button that opens a menu with options to copy information from an item.",
|
||||||
|
"placeholders": {
|
||||||
|
"itemname": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Secret Item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moreOptions": {
|
||||||
|
"message": "More options, $ITEMNAME$",
|
||||||
|
"description": "Aria label for a button that opens a menu with more options for an item.",
|
||||||
|
"placeholders": {
|
||||||
|
"itemname": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Secret Item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"adminConsole": {
|
"adminConsole": {
|
||||||
"message": "Admin Console"
|
"message": "Admin Console"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CommonModule, Location } from "@angular/common";
|
import { CommonModule, Location } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||||
import { Observable, combineLatest, switchMap } from "rxjs";
|
import { Observable, combineLatest, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@@ -23,7 +23,7 @@ export type CurrentAccount = {
|
|||||||
selector: "app-current-account",
|
selector: "app-current-account",
|
||||||
templateUrl: "current-account.component.html",
|
templateUrl: "current-account.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, JslibModule, AvatarModule],
|
imports: [CommonModule, JslibModule, AvatarModule, RouterModule],
|
||||||
})
|
})
|
||||||
export class CurrentAccountComponent {
|
export class CurrentAccountComponent {
|
||||||
currentAccount$: Observable<CurrentAccount>;
|
currentAccount$: Observable<CurrentAccount>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mock, MockProxy, mockReset } from "jest-mock-extended";
|
import { mock, mockReset, MockProxy } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||||
@@ -65,6 +65,7 @@ const mockEquivalentDomains = [
|
|||||||
describe("AutofillService", () => {
|
describe("AutofillService", () => {
|
||||||
let autofillService: AutofillService;
|
let autofillService: AutofillService;
|
||||||
const cipherService = mock<CipherService>();
|
const cipherService = mock<CipherService>();
|
||||||
|
let inlineMenuVisibilityMock$!: BehaviorSubject<InlineMenuVisibilitySetting>;
|
||||||
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
||||||
const mockUserId = Utils.newGuid() as UserId;
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
@@ -77,13 +78,12 @@ describe("AutofillService", () => {
|
|||||||
const userVerificationService = mock<UserVerificationService>();
|
const userVerificationService = mock<UserVerificationService>();
|
||||||
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||||
const platformUtilsService = mock<PlatformUtilsService>();
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
let inlineMenuVisibilitySettingMock$!: BehaviorSubject<InlineMenuVisibilitySetting>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||||
inlineMenuVisibilitySettingMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
autofillSettingsService = mock<AutofillSettingsServiceAbstraction>();
|
autofillSettingsService = mock<AutofillSettingsServiceAbstraction>();
|
||||||
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilitySettingMock$;
|
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
|
||||||
autofillService = new AutofillService(
|
autofillService = new AutofillService(
|
||||||
cipherService,
|
cipherService,
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
@@ -151,17 +151,92 @@ describe("AutofillService", () => {
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handle inline menu visibility change", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await autofillService.loadAutofillScriptsOnInstall();
|
||||||
|
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([tab1, tab2]);
|
||||||
|
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
|
||||||
|
jest.spyOn(autofillService, "reloadAutofillScripts").mockImplementation();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early if the setting is being initialized", async () => {
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabsQuery).toHaveBeenCalledTimes(1);
|
||||||
|
expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early if the previous setting is equivalent to the new setting", async () => {
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabsQuery).toHaveBeenCalledTimes(1);
|
||||||
|
expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updates the inline menu visibility setting", () => {
|
||||||
|
it("when changing the inline menu from on focus of field to on button click", async () => {
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
||||||
|
tab1,
|
||||||
|
"updateAutofillInlineMenuVisibility",
|
||||||
|
{ inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
|
||||||
|
);
|
||||||
|
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
||||||
|
tab2,
|
||||||
|
"updateAutofillInlineMenuVisibility",
|
||||||
|
{ inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when changing the inline menu from button click to field focus", async () => {
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
||||||
|
tab1,
|
||||||
|
"updateAutofillInlineMenuVisibility",
|
||||||
|
{ inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus },
|
||||||
|
);
|
||||||
|
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
|
||||||
|
tab2,
|
||||||
|
"updateAutofillInlineMenuVisibility",
|
||||||
|
{ inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reloads the autofill scripts", () => {
|
||||||
|
it("when changing the inline menu from a disabled setting to an enabled setting", async () => {
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.Off);
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillService.reloadAutofillScripts).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when changing the inline menu from a enabled setting to a disabled setting", async () => {
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.Off);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillService.reloadAutofillScripts).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("reloadAutofillScripts", () => {
|
describe("reloadAutofillScripts", () => {
|
||||||
it("disconnects and removes all autofill script ports", () => {
|
it("re-injects the autofill scripts in all tabs and disconnects all connected ports", () => {
|
||||||
const port1 = mock<chrome.runtime.Port>({
|
const port1 = mock<chrome.runtime.Port>();
|
||||||
disconnect: jest.fn(),
|
const port2 = mock<chrome.runtime.Port>();
|
||||||
});
|
|
||||||
const port2 = mock<chrome.runtime.Port>({
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
});
|
|
||||||
autofillService["autofillScriptPortsSet"] = new Set([port1, port2]);
|
autofillService["autofillScriptPortsSet"] = new Set([port1, port2]);
|
||||||
|
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
||||||
|
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@@ -170,17 +245,6 @@ describe("AutofillService", () => {
|
|||||||
expect(port1.disconnect).toHaveBeenCalled();
|
expect(port1.disconnect).toHaveBeenCalled();
|
||||||
expect(port2.disconnect).toHaveBeenCalled();
|
expect(port2.disconnect).toHaveBeenCalled();
|
||||||
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||||
});
|
|
||||||
|
|
||||||
it("re-injects the autofill scripts in all tabs", () => {
|
|
||||||
autofillService["autofillScriptPortsSet"] = new Set([mock<chrome.runtime.Port>()]);
|
|
||||||
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
autofillService.reloadAutofillScripts();
|
|
||||||
|
|
||||||
expect(autofillService["injectAutofillScriptsInAllTabs"]).toHaveBeenCalled();
|
expect(autofillService["injectAutofillScriptsInAllTabs"]).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2095,6 +2095,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the autofill inline menu visibility setting in all active tabs
|
||||||
|
* when the InlineMenuVisibilitySetting observable is updated.
|
||||||
|
*
|
||||||
|
* @param previousSetting - The previous setting value
|
||||||
|
* @param currentSetting - The current setting value
|
||||||
|
*/
|
||||||
private async handleInlineMenuVisibilityChange(
|
private async handleInlineMenuVisibilityChange(
|
||||||
previousSetting: InlineMenuVisibilitySetting,
|
previousSetting: InlineMenuVisibilitySetting,
|
||||||
currentSetting: InlineMenuVisibilitySetting,
|
currentSetting: InlineMenuVisibilitySetting,
|
||||||
|
|||||||
@@ -1319,6 +1319,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement);
|
const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement);
|
||||||
|
if (!cachedAutofillFieldElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
cachedAutofillFieldElement.viewable = true;
|
cachedAutofillFieldElement.viewable = true;
|
||||||
|
|
||||||
void this.autofillOverlayContentService?.setupAutofillInlineMenuListenerOnField(
|
void this.autofillOverlayContentService?.setupAutofillInlineMenuListenerOnField(
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export default class RuntimeBackground {
|
|||||||
|
|
||||||
if (msg.command === "loggedIn") {
|
if (msg.command === "loggedIn") {
|
||||||
await this.sendBwInstalledMessageToVault();
|
await this.sendBwInstalledMessageToVault();
|
||||||
|
await this.autofillService.reloadAutofillScripts();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo
|
|||||||
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||||
import { ExportComponent } from "../tools/popup/settings/export.component";
|
import { ExportComponent } from "../tools/popup/settings/export.component";
|
||||||
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
|
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
|
||||||
|
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
|
||||||
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
||||||
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
||||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||||
@@ -54,6 +55,7 @@ import { AppearanceComponent } from "../vault/popup/settings/appearance.componen
|
|||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||||
|
|
||||||
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
||||||
@@ -263,12 +265,11 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "notifications" },
|
data: { state: "notifications" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, {
|
||||||
path: "vault-settings",
|
path: "vault-settings",
|
||||||
component: VaultSettingsComponent,
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "vault-settings" },
|
data: { state: "vault-settings" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "folders",
|
path: "folders",
|
||||||
component: FoldersComponent,
|
component: FoldersComponent,
|
||||||
@@ -381,12 +382,11 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "tabs_generator" },
|
data: { state: "tabs_generator" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(SettingsComponent, SettingsV2Component, {
|
||||||
path: "settings",
|
path: "settings",
|
||||||
component: SettingsComponent,
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "tabs_settings" },
|
data: { state: "tabs_settings" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "send",
|
path: "send",
|
||||||
component: SendGroupingsComponent,
|
component: SendGroupingsComponent,
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component"
|
|||||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||||
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
||||||
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
|
||||||
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
@@ -190,7 +189,6 @@ import "../platform/popup/locales";
|
|||||||
AutofillComponent,
|
AutofillComponent,
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
AccountSwitcherComponent,
|
AccountSwitcherComponent,
|
||||||
VaultV2Component,
|
|
||||||
],
|
],
|
||||||
providers: [CurrencyPipe, DatePipe],
|
providers: [CurrencyPipe, DatePipe],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="{{ 'settings' | i18n }}">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
<app-current-account></app-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<bit-item-group>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/account-security"
|
||||||
|
>{{ "accountSecurity" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/autofill"
|
||||||
|
>{{ "autofill" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/notifications"
|
||||||
|
>{{ "notifications" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/vault-settings"
|
||||||
|
>{{ "vault" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/appearance"
|
||||||
|
>{{ "appearance" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/about"
|
||||||
|
>{{ "about" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</popup-page>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { ItemModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "settings-v2.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
RouterModule,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopOutComponent,
|
||||||
|
ItemModule,
|
||||||
|
CurrentAccountComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SettingsV2Component {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<app-vault-list-items-container
|
||||||
|
*ngIf="autofillCiphers$ | async as ciphers"
|
||||||
|
[ciphers]="ciphers"
|
||||||
|
[title]="'autofillSuggestions' | i18n"
|
||||||
|
showAutoFill
|
||||||
|
></app-vault-list-items-container>
|
||||||
|
<ng-container *ngIf="showEmptyAutofillTip$ | async">
|
||||||
|
<bit-section>
|
||||||
|
<popup-section-header [title]="'autofillSuggestions' | i18n"></popup-section-header>
|
||||||
|
<span class="tw-text-muted tw-px-1" bitTypography="body2">{{
|
||||||
|
"autofillSuggestionsTip" | i18n
|
||||||
|
}}</span>
|
||||||
|
</bit-section>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { combineLatest, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { SectionComponent, TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
||||||
|
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||||
|
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SectionComponent,
|
||||||
|
TypographyModule,
|
||||||
|
VaultListItemsContainerComponent,
|
||||||
|
JslibModule,
|
||||||
|
PopupSectionHeaderComponent,
|
||||||
|
],
|
||||||
|
selector: "app-autofill-vault-list-items",
|
||||||
|
templateUrl: "autofill-vault-list-items.component.html",
|
||||||
|
})
|
||||||
|
export class AutofillVaultListItemsComponent {
|
||||||
|
/**
|
||||||
|
* The list of ciphers that can be used to autofill the current page.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected autofillCiphers$: Observable<CipherView[]> =
|
||||||
|
this.vaultPopupItemsService.autoFillCiphers$;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that determines whether the empty autofill tip should be shown.
|
||||||
|
* The tip is shown when there are no ciphers to autofill, no filter is applied, and autofill is allowed in
|
||||||
|
* the current context (e.g. not in a popout).
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected showEmptyAutofillTip$: Observable<boolean> = combineLatest([
|
||||||
|
this.vaultPopupItemsService.hasFilterApplied$,
|
||||||
|
this.autofillCiphers$,
|
||||||
|
this.vaultPopupItemsService.autofillAllowed$,
|
||||||
|
]).pipe(
|
||||||
|
map(([hasFilter, ciphers, canAutoFill]) => !hasFilter && canAutoFill && ciphers.length === 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
||||||
|
// TODO: Migrate logic to show Autofill policy toast PM-8144
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./vault-list-items-container/vault-list-items-container.component";
|
||||||
|
export * from "./autofill-vault-list-items/autofill-vault-list-items.component";
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<bit-section *ngIf="ciphers?.length > 0">
|
||||||
|
<popup-section-header [title]="title">
|
||||||
|
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||||
|
</popup-section-header>
|
||||||
|
<bit-item-group>
|
||||||
|
<bit-item *ngFor="let cipher of ciphers">
|
||||||
|
<a bit-item-content [routerLink]="['/view-cipher']" [queryParams]="{ cipherId: cipher.id }">
|
||||||
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
|
{{ cipher.name }}
|
||||||
|
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
|
</a>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action *ngIf="showAutoFill">
|
||||||
|
<button type="button" bitBadge variant="primary">{{ "autoFill" | i18n }}</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
size="small"
|
||||||
|
[attr.aria-label]="'copyInfo' | i18n: cipher.name"
|
||||||
|
></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
[attr.aria-label]="'moreOptions' | i18n: cipher.name"
|
||||||
|
></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</bit-section>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||||
|
import { RouterLink } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
SectionComponent,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ItemModule,
|
||||||
|
ButtonModule,
|
||||||
|
BadgeModule,
|
||||||
|
IconButtonModule,
|
||||||
|
SectionComponent,
|
||||||
|
TypographyModule,
|
||||||
|
JslibModule,
|
||||||
|
PopupSectionHeaderComponent,
|
||||||
|
RouterLink,
|
||||||
|
],
|
||||||
|
selector: "app-vault-list-items-container",
|
||||||
|
templateUrl: "vault-list-items-container.component.html",
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class VaultListItemsContainerComponent {
|
||||||
|
@Input()
|
||||||
|
ciphers: CipherView[];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Input({ transform: booleanAttribute })
|
||||||
|
showAutoFill: boolean;
|
||||||
|
}
|
||||||
@@ -10,4 +10,40 @@
|
|||||||
<app-current-account></app-current-account>
|
<app-current-account></app-current-account>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</popup-header>
|
</popup-header>
|
||||||
|
|
||||||
|
<div *ngIf="showEmptyState$ | async" class="tw-flex tw-flex-col tw-h-full tw-justify-center">
|
||||||
|
<bit-no-items [icon]="vaultIcon">
|
||||||
|
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container>
|
||||||
|
<button slot="button" type="button" bitButton buttonType="primary" (click)="addCipher()">
|
||||||
|
{{ "new" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-no-items>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!(showEmptyState$ | async)">
|
||||||
|
<!-- TODO: Filter/search Section in PM-6824 and PM-6826.-->
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="showNoResultsState$ | async"
|
||||||
|
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||||
|
>
|
||||||
|
<bit-no-items>
|
||||||
|
<ng-container slot="title">{{ "noItemsMatchSearch" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">{{ "clearFiltersOrTryAnother" | i18n }}</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!(showNoResultsState$ | async)">
|
||||||
|
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
||||||
|
<app-vault-list-items-container
|
||||||
|
[title]="'favorites' | i18n"
|
||||||
|
[ciphers]="favoriteCiphers$ | async"
|
||||||
|
></app-vault-list-items-container>
|
||||||
|
<app-vault-list-items-container
|
||||||
|
[title]="'allItems' | i18n"
|
||||||
|
[ciphers]="remainingCiphers$ | async"
|
||||||
|
></app-vault-list-items-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</popup-page>
|
</popup-page>
|
||||||
|
|||||||
@@ -1,13 +1,55 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { Router, RouterLink } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
||||||
|
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||||
|
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||||
|
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault",
|
selector: "app-vault",
|
||||||
templateUrl: "vault-v2.component.html",
|
templateUrl: "vault-v2.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopOutComponent,
|
||||||
|
CurrentAccountComponent,
|
||||||
|
NoItemsModule,
|
||||||
|
JslibModule,
|
||||||
|
CommonModule,
|
||||||
|
AutofillVaultListItemsComponent,
|
||||||
|
VaultListItemsContainerComponent,
|
||||||
|
ButtonModule,
|
||||||
|
RouterLink,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultV2Component implements OnInit, OnDestroy {
|
export class VaultV2Component implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||||
|
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||||
|
|
||||||
|
protected showEmptyState$ = this.vaultPopupItemsService.emptyVault$;
|
||||||
|
protected showNoResultsState$ = this.vaultPopupItemsService.noFilteredResults$;
|
||||||
|
|
||||||
|
protected vaultIcon = Icons.Vault;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private vaultPopupItemsService: VaultPopupItemsService,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
|
|
||||||
ngOnDestroy(): void {}
|
ngOnDestroy(): void {}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
// TODO: Add currently filtered organization to query params if available
|
||||||
|
void this.router.navigate(["/add-cipher"], {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
import { VaultPopupItemsService } from "./vault-popup-items.service";
|
||||||
|
|
||||||
|
describe("VaultPopupItemsService", () => {
|
||||||
|
let service: VaultPopupItemsService;
|
||||||
|
let allCiphers: Record<CipherId, CipherView>;
|
||||||
|
let autoFillCiphers: CipherView[];
|
||||||
|
|
||||||
|
const cipherServiceMock = mock<CipherService>();
|
||||||
|
const vaultSettingsServiceMock = mock<VaultSettingsService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
allCiphers = cipherFactory(10);
|
||||||
|
const cipherList = Object.values(allCiphers);
|
||||||
|
// First 2 ciphers are autofill
|
||||||
|
autoFillCiphers = cipherList.slice(0, 2);
|
||||||
|
|
||||||
|
// First autofill cipher is also favorite
|
||||||
|
autoFillCiphers[0].favorite = true;
|
||||||
|
|
||||||
|
// 3rd and 4th ciphers are favorite
|
||||||
|
cipherList[2].favorite = true;
|
||||||
|
cipherList[3].favorite = true;
|
||||||
|
|
||||||
|
cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
|
||||||
|
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
|
||||||
|
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
|
||||||
|
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||||
|
jest
|
||||||
|
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
||||||
|
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
||||||
|
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be created", () => {
|
||||||
|
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("autoFillCiphers$", () => {
|
||||||
|
it("should return empty array if there is no current tab", (done) => {
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
||||||
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(ciphers).toEqual([]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array if in Popout window", (done) => {
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||||
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(ciphers).toEqual([]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter ciphers for the current tab and types", (done) => {
|
||||||
|
const currentTab = { url: "https://example.com" } as chrome.tabs.Tab;
|
||||||
|
|
||||||
|
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(true).asObservable();
|
||||||
|
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
|
||||||
|
|
||||||
|
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
||||||
|
|
||||||
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
|
||||||
|
expect(cipherServiceMock.filterCiphersForUrl).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
currentTab.url,
|
||||||
|
[CipherType.Card, CipherType.Identity],
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ciphers sorted by type, then by last used date, then by name", (done) => {
|
||||||
|
const expectedTypeOrder: Record<CipherType, number> = {
|
||||||
|
[CipherType.Login]: 1,
|
||||||
|
[CipherType.Card]: 2,
|
||||||
|
[CipherType.Identity]: 3,
|
||||||
|
[CipherType.SecureNote]: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assume all ciphers are autofill ciphers to test sorting
|
||||||
|
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () =>
|
||||||
|
Object.values(allCiphers),
|
||||||
|
);
|
||||||
|
|
||||||
|
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
||||||
|
|
||||||
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(ciphers.length).toBe(10);
|
||||||
|
|
||||||
|
for (let i = 0; i < ciphers.length - 1; i++) {
|
||||||
|
const current = ciphers[i];
|
||||||
|
const next = ciphers[i + 1];
|
||||||
|
|
||||||
|
expect(expectedTypeOrder[current.type]).toBeLessThanOrEqual(expectedTypeOrder[next.type]);
|
||||||
|
}
|
||||||
|
expect(cipherServiceMock.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("favoriteCiphers$", () => {
|
||||||
|
it("should exclude autofill ciphers", (done) => {
|
||||||
|
service.favoriteCiphers$.subscribe((ciphers) => {
|
||||||
|
// 2 autofill ciphers, 3 favorite ciphers, 1 favorite cipher is also autofill = 2 favorite ciphers to show
|
||||||
|
expect(ciphers.length).toBe(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sort by last used then by name", (done) => {
|
||||||
|
service.favoriteCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(cipherServiceMock.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("remainingCiphers$", () => {
|
||||||
|
it("should exclude autofill and favorite ciphers", (done) => {
|
||||||
|
service.remainingCiphers$.subscribe((ciphers) => {
|
||||||
|
// 2 autofill ciphers, 2 favorite ciphers = 6 remaining ciphers to show
|
||||||
|
expect(ciphers.length).toBe(6);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sort by last used then by name", (done) => {
|
||||||
|
service.remainingCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(cipherServiceMock.getLocaleSortingFunction).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("emptyVault$", () => {
|
||||||
|
it("should return true if there are no ciphers", (done) => {
|
||||||
|
cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
|
||||||
|
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
||||||
|
service.emptyVault$.subscribe((empty) => {
|
||||||
|
expect(empty).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if there are ciphers", (done) => {
|
||||||
|
service.emptyVault$.subscribe((empty) => {
|
||||||
|
expect(empty).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("autoFillAllowed$", () => {
|
||||||
|
it("should return true if there is a current tab", (done) => {
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if there is no current tab", (done) => {
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if in a Popout", (done) => {
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||||
|
service.autofillAllowed$.subscribe((allowed) => {
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// A function to generate a list of ciphers of different types
|
||||||
|
function cipherFactory(count: number): Record<CipherId, CipherView> {
|
||||||
|
const ciphers: CipherView[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const type = ((i % 4) + 1) as CipherType;
|
||||||
|
switch (type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
ciphers.push({
|
||||||
|
id: `${i}`,
|
||||||
|
type: CipherType.Login,
|
||||||
|
name: `Login ${i}`,
|
||||||
|
login: {
|
||||||
|
username: `username${i}`,
|
||||||
|
password: `password${i}`,
|
||||||
|
},
|
||||||
|
} as CipherView);
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
ciphers.push({
|
||||||
|
id: `${i}`,
|
||||||
|
type: CipherType.SecureNote,
|
||||||
|
name: `SecureNote ${i}`,
|
||||||
|
notes: `notes${i}`,
|
||||||
|
} as CipherView);
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
ciphers.push({
|
||||||
|
id: `${i}`,
|
||||||
|
type: CipherType.Card,
|
||||||
|
name: `Card ${i}`,
|
||||||
|
card: {
|
||||||
|
cardholderName: `cardholderName${i}`,
|
||||||
|
number: `number${i}`,
|
||||||
|
brand: `brand${i}`,
|
||||||
|
},
|
||||||
|
} as CipherView);
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
ciphers.push({
|
||||||
|
id: `${i}`,
|
||||||
|
type: CipherType.Identity,
|
||||||
|
name: `Identity ${i}`,
|
||||||
|
identity: {
|
||||||
|
firstName: `firstName${i}`,
|
||||||
|
lastName: `lastName${i}`,
|
||||||
|
},
|
||||||
|
} as CipherView);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.fromEntries(ciphers.map((c) => [c.id, c]));
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing the various item lists on the new Vault tab in the browser popup.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class VaultPopupItemsService {
|
||||||
|
private _refreshCurrentTab$ = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that contains the list of other cipher types that should be shown
|
||||||
|
* in the autofill section of the Vault tab. Depends on vault settings.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _otherAutoFillTypes$: Observable<CipherType[]> = combineLatest([
|
||||||
|
this.vaultSettingsService.showCardsCurrentTab$,
|
||||||
|
this.vaultSettingsService.showIdentitiesCurrentTab$,
|
||||||
|
]).pipe(
|
||||||
|
map(([showCards, showIdentities]) => {
|
||||||
|
return [
|
||||||
|
...(showCards ? [CipherType.Card] : []),
|
||||||
|
...(showIdentities ? [CipherType.Identity] : []),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that contains the current tab to be considered for autofill. If there is no current tab
|
||||||
|
* or the popup is in a popout window, this will be null.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _currentAutofillTab$: Observable<chrome.tabs.Tab | null> = this._refreshCurrentTab$.pipe(
|
||||||
|
startWith(null),
|
||||||
|
switchMap(async () => {
|
||||||
|
if (BrowserPopupUtils.inPopout(window)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that contains the list of all decrypted ciphers.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _cipherList$: Observable<CipherView[]> = this.cipherService.cipherViews$.pipe(
|
||||||
|
map((ciphers) => Object.values(ciphers)),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities
|
||||||
|
* if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name.
|
||||||
|
*
|
||||||
|
* See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
|
||||||
|
*/
|
||||||
|
autoFillCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
|
this._cipherList$,
|
||||||
|
this._otherAutoFillTypes$,
|
||||||
|
this._currentAutofillTab$,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([ciphers, otherTypes, tab]) => {
|
||||||
|
if (!tab) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
return this.cipherService.filterCiphersForUrl(ciphers, tab.url, otherTypes);
|
||||||
|
}),
|
||||||
|
map((ciphers) => ciphers.sort(this.sortCiphersForAutofill.bind(this))),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of favorite ciphers that are not currently suggested for autofill.
|
||||||
|
* Ciphers are sorted by last used date, then by name.
|
||||||
|
*/
|
||||||
|
favoriteCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
|
this.autoFillCiphers$,
|
||||||
|
this._cipherList$,
|
||||||
|
]).pipe(
|
||||||
|
map(([autoFillCiphers, ciphers]) =>
|
||||||
|
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
||||||
|
),
|
||||||
|
map((ciphers) =>
|
||||||
|
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)),
|
||||||
|
),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
|
||||||
|
* Ciphers are sorted by name.
|
||||||
|
*/
|
||||||
|
remainingCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
|
this.autoFillCiphers$,
|
||||||
|
this.favoriteCiphers$,
|
||||||
|
this._cipherList$,
|
||||||
|
]).pipe(
|
||||||
|
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
|
||||||
|
ciphers.filter(
|
||||||
|
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
map((ciphers) => ciphers.sort(this.cipherService.getLocaleSortingFunction())),
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether a filter is currently applied to the ciphers.
|
||||||
|
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
||||||
|
*/
|
||||||
|
hasFilterApplied$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether autofill is allowed in the current context.
|
||||||
|
* Autofill is allowed when there is a current tab and the popup is not in a popout window.
|
||||||
|
*/
|
||||||
|
autofillAllowed$: Observable<boolean> = this._currentAutofillTab$.pipe(map((tab) => !!tab));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether the user's vault is empty.
|
||||||
|
*/
|
||||||
|
emptyVault$: Observable<boolean> = this._cipherList$.pipe(map((ciphers) => !ciphers.length));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether there are no ciphers to show with the current filter.
|
||||||
|
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
||||||
|
*/
|
||||||
|
noFilteredResults$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-fetch the current tab to trigger a re-evaluation of the autofill ciphers.
|
||||||
|
*/
|
||||||
|
refreshCurrentTab() {
|
||||||
|
this._refreshCurrentTab$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort function for ciphers to be used in the autofill section of the Vault tab.
|
||||||
|
* Sorts by type, then by last used date, and finally by name.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private sortCiphersForAutofill(a: CipherView, b: CipherView): number {
|
||||||
|
const typeOrder: Record<CipherType, number> = {
|
||||||
|
[CipherType.Login]: 1,
|
||||||
|
[CipherType.Card]: 2,
|
||||||
|
[CipherType.Identity]: 3,
|
||||||
|
[CipherType.SecureNote]: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compare types first
|
||||||
|
if (typeOrder[a.type] < typeOrder[b.type]) {
|
||||||
|
return -1;
|
||||||
|
} else if (typeOrder[a.type] > typeOrder[b.type]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If types are the same, then sort by last used then name
|
||||||
|
return this.cipherService.sortCiphersByLastUsedThenName(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" [pageTitle]="'vault' | i18n" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<bit-item-group>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/folders">
|
||||||
|
{{ "folders" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button type="button" bit-item-content (click)="import()">
|
||||||
|
{{ "importItems" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-popout" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/export">
|
||||||
|
{{ "exportVault" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button type="button" bit-item-content (click)="sync()">
|
||||||
|
{{ "syncVaultNow" | i18n }}
|
||||||
|
<span slot="secondary">{{ lastSync }}</span>
|
||||||
|
<i slot="end" class="bwi bwi-refresh" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</popup-page>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { Router, RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "vault-settings-v2.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
RouterModule,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopOutComponent,
|
||||||
|
ItemModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class VaultSettingsV2Component implements OnInit {
|
||||||
|
lastSync = "--";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.setLastSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async import() {
|
||||||
|
await this.router.navigate(["/import"]);
|
||||||
|
if (await BrowserApi.isPopupOpen()) {
|
||||||
|
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync() {
|
||||||
|
let toastConfig: ToastOptions;
|
||||||
|
const success = await this.syncService.fullSync(true);
|
||||||
|
if (success) {
|
||||||
|
await this.setLastSync();
|
||||||
|
toastConfig = {
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("syncingComplete"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
toastConfig = { variant: "error", title: "", message: this.i18nService.t("syncingFailed") };
|
||||||
|
}
|
||||||
|
this.toastService.showToast(toastConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setLastSync() {
|
||||||
|
const last = await this.syncService.getLastSync();
|
||||||
|
if (last != null) {
|
||||||
|
this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString();
|
||||||
|
} else {
|
||||||
|
this.lastSync = this.i18nService.t("never");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
||||||
const config = require("../../libs/components/tailwind.config.base");
|
const config = require("../../libs/components/tailwind.config.base");
|
||||||
|
|
||||||
config.content = ["./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}"];
|
config.content = [
|
||||||
|
"./src/**/*.{html,ts}",
|
||||||
|
"../../libs/components/src/**/*.{html,ts}",
|
||||||
|
"../../libs/angular/src/**/*.{html,ts}",
|
||||||
|
];
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -577,6 +577,17 @@ app-vault-view .box-footer {
|
|||||||
user-select: auto;
|
user-select: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* override for vault icon in desktop */
|
||||||
|
app-vault-icon > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
float: left;
|
||||||
|
height: 36px;
|
||||||
|
width: 34px;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* tweak for inconsistent line heights in cipher view */
|
/* tweak for inconsistent line heights in cipher view */
|
||||||
.box-footer button,
|
.box-footer button,
|
||||||
.box-footer a {
|
.box-footer a {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
||||||
const config = require("../../libs/components/tailwind.config.base");
|
const config = require("../../libs/components/tailwind.config.base");
|
||||||
|
|
||||||
config.content = ["./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}"];
|
config.content = [
|
||||||
|
"./src/**/*.{html,ts}",
|
||||||
|
"../../libs/components/src/**/*.{html,ts}",
|
||||||
|
"../../libs/angular/src/**/*.{html,ts}",
|
||||||
|
];
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
<bit-container>
|
<bit-container>
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<table class="table table-hover table-list" *ngIf="!loading">
|
<bit-table *ngIf="!loading">
|
||||||
<tbody>
|
<ng-template body>
|
||||||
<tr *ngFor="let p of policies">
|
<tr bitRow *ngFor="let p of policies">
|
||||||
<td *ngIf="p.display(organization)">
|
<td bitCell *ngIf="p.display(organization)">
|
||||||
<a href="#" appStopClick (click)="edit(p)">{{ p.name | i18n }}</a>
|
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
|
||||||
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
|
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
|
||||||
"on" | i18n
|
"on" | i18n
|
||||||
}}</span>
|
}}</span>
|
||||||
<small class="text-muted d-block">{{ p.description | i18n }}</small>
|
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</ng-template>
|
||||||
</table>
|
</bit-table>
|
||||||
<ng-template #editTemplate></ng-template>
|
<ng-template #editTemplate></ng-template>
|
||||||
</bit-container>
|
</bit-container>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<app-change-kdf *ngIf="showChangeKdf"></app-change-kdf>
|
<app-change-kdf *ngIf="showChangeKdf"></app-change-kdf>
|
||||||
<div
|
<div
|
||||||
[ngClass]="{ 'tabbed-header': !showChangeKdf, 'secondary-header': showChangeKdf }"
|
[ngClass]="{ 'tw-mt-6': !showChangeKdf, 'tw-mt-16': showChangeKdf }"
|
||||||
class="border-0 mb-0"
|
class="tw-border-0 tw-mb-0 tw-pb-2.5"
|
||||||
>
|
>
|
||||||
<h1>{{ "apiKey" | i18n }}</h1>
|
<h2 bitTypography="h2">{{ "apiKey" | i18n }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p bitTypography="body1">
|
||||||
{{ "userApiKeyDesc" | i18n }}
|
{{ "userApiKeyDesc" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="updateLicenseForm" [bitSubmit]="submit">
|
||||||
<div class="form-group">
|
<bit-form-field>
|
||||||
<label for="file" class="sr-only">{{ "licenseFile" | i18n }}</label>
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
<div>
|
||||||
<small class="form-text text-muted">{{
|
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||||
|
{{ "chooseFile" | i18n }}
|
||||||
|
</button>
|
||||||
|
{{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
#fileSelector
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="setSelectedFile($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<bit-hint>{{
|
||||||
"licenseFileDesc"
|
"licenseFileDesc"
|
||||||
| i18n
|
| i18n
|
||||||
: (!organizationId
|
: (!organizationId
|
||||||
? "bitwarden_premium_license.json"
|
? "bitwarden_premium_license.json"
|
||||||
: "bitwarden_organization_license.json")
|
: "bitwarden_organization_license.json")
|
||||||
}}</small>
|
}}</bit-hint>
|
||||||
</div>
|
</bit-form-field>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
{{ "submit" | i18n }}
|
||||||
<span>{{ "submit" | i18n }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="showCancel" type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
<button
|
||||||
|
bitButton
|
||||||
|
*ngIf="showCancel"
|
||||||
|
bitFormButton
|
||||||
|
buttonType="secondary"
|
||||||
|
type="button"
|
||||||
|
[bitAction]="cancel"
|
||||||
|
>
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -17,19 +17,30 @@ export class UpdateLicenseComponent {
|
|||||||
@Output() onCanceled = new EventEmitter();
|
@Output() onCanceled = new EventEmitter();
|
||||||
|
|
||||||
formPromise: Promise<void>;
|
formPromise: Promise<void>;
|
||||||
|
title: string = this.i18nService.t("updateLicense");
|
||||||
|
updateLicenseForm = this.formBuilder.group({
|
||||||
|
file: [null, Validators.required],
|
||||||
|
});
|
||||||
|
licenseFile: File = null;
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
) {}
|
) {}
|
||||||
|
protected setSelectedFile(event: Event) {
|
||||||
async submit() {
|
const fileInputEl = <HTMLInputElement>event.target;
|
||||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||||
const files = fileEl.files;
|
this.licenseFile = file;
|
||||||
if (files == null || files.length === 0) {
|
}
|
||||||
|
submit = async () => {
|
||||||
|
this.updateLicenseForm.markAllAsTouched();
|
||||||
|
if (this.updateLicenseForm.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const files = this.licenseFile;
|
||||||
|
if (files == null) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
@@ -37,10 +48,8 @@ export class UpdateLicenseComponent {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("license", files[0]);
|
fd.append("license", files);
|
||||||
|
|
||||||
let updatePromise: Promise<void | unknown> = null;
|
let updatePromise: Promise<void | unknown> = null;
|
||||||
if (this.organizationId == null) {
|
if (this.organizationId == null) {
|
||||||
@@ -60,12 +69,9 @@ export class UpdateLicenseComponent {
|
|||||||
this.i18nService.t("licenseUploadSuccess"),
|
this.i18nService.t("licenseUploadSuccess"),
|
||||||
);
|
);
|
||||||
this.onUpdated.emit();
|
this.onUpdated.emit();
|
||||||
} catch (e) {
|
};
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
cancel = () => {
|
||||||
this.onCanceled.emit();
|
this.onCanceled.emit();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<span bitDialogContent>
|
<span bitDialogContent>
|
||||||
<ng-container *ngIf="!permanent">
|
<ng-container *ngIf="!permanent">
|
||||||
<span *ngIf="cipherIds?.length">
|
<span *ngIf="cipherIds?.length || unassignedCiphers?.length">
|
||||||
{{ "deleteSelectedItemsDesc" | i18n: cipherIds.length }}
|
{{ "deleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="collections?.length">
|
<span *ngIf="collections?.length">
|
||||||
{{ "deleteSelectedCollectionsDesc" | i18n: collections.length }}
|
{{ "deleteSelectedCollectionsDesc" | i18n: collections.length }}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ "deleteSelectedConfirmation" | i18n }}
|
{{ "deleteSelectedConfirmation" | i18n }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="permanent">
|
<ng-container *ngIf="permanent">
|
||||||
{{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length }}
|
{{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface BulkDeleteDialogParams {
|
|||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
organizations?: Organization[];
|
organizations?: Organization[];
|
||||||
collections?: CollectionView[];
|
collections?: CollectionView[];
|
||||||
|
unassignedCiphers?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BulkDeleteDialogResult {
|
export enum BulkDeleteDialogResult {
|
||||||
@@ -51,6 +52,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
organization: Organization;
|
organization: Organization;
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
collections: CollectionView[];
|
collections: CollectionView[];
|
||||||
|
unassignedCiphers: string[];
|
||||||
|
|
||||||
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
@@ -75,6 +77,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
this.organization = params.organization;
|
this.organization = params.organization;
|
||||||
this.organizations = params.organizations;
|
this.organizations = params.organizations;
|
||||||
this.collections = params.collections;
|
this.collections = params.collections;
|
||||||
|
this.unassignedCiphers = params.unassignedCiphers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async cancel() {
|
protected async cancel() {
|
||||||
@@ -83,6 +86,15 @@ export class BulkDeleteDialogComponent {
|
|||||||
|
|
||||||
protected submit = async () => {
|
protected submit = async () => {
|
||||||
const deletePromises: Promise<void>[] = [];
|
const deletePromises: Promise<void>[] = [];
|
||||||
|
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||||
|
|
||||||
|
// Unassigned ciphers under an Owner/Admin OR Custom Users With Edit will call the deleteCiphersAdmin method
|
||||||
|
if (
|
||||||
|
this.unassignedCiphers.length &&
|
||||||
|
this.organization.canEditUnassignedCiphers(restrictProviderAccess)
|
||||||
|
) {
|
||||||
|
deletePromises.push(this.deleteCiphersAdmin(this.unassignedCiphers));
|
||||||
|
}
|
||||||
if (this.cipherIds.length) {
|
if (this.cipherIds.length) {
|
||||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||||
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||||
@@ -93,7 +105,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
) {
|
) {
|
||||||
deletePromises.push(this.deleteCiphers());
|
deletePromises.push(this.deleteCiphers());
|
||||||
} else {
|
} else {
|
||||||
deletePromises.push(this.deleteCiphersAdmin());
|
deletePromises.push(this.deleteCiphersAdmin(this.cipherIds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +115,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
|
|
||||||
await Promise.all(deletePromises);
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
if (this.cipherIds.length) {
|
if (this.cipherIds.length || this.unassignedCiphers.length) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
@@ -135,8 +147,8 @@ export class BulkDeleteDialogComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteCiphersAdmin(): Promise<any> {
|
private async deleteCiphersAdmin(ciphers: string[]): Promise<any> {
|
||||||
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
|
const deleteRequest = new CipherBulkDeleteRequest(ciphers, this.organization.id);
|
||||||
if (this.permanent) {
|
if (this.permanent) {
|
||||||
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -751,7 +751,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
if (ciphers.length === 1 && collections.length === 0) {
|
if (ciphers.length === 1 && collections.length === 0) {
|
||||||
await this.deleteCipher(ciphers[0]);
|
await this.deleteCipher(ciphers[0]);
|
||||||
} else if (ciphers.length === 0 && collections.length === 1) {
|
} else if (ciphers.length === 0 && collections.length === 1) {
|
||||||
await this.deleteCollection(collections[0]);
|
await this.deleteCollection(collections[0] as CollectionAdminView);
|
||||||
} else {
|
} else {
|
||||||
await this.bulkDelete(ciphers, collections, this.organization);
|
await this.bulkDelete(ciphers, collections, this.organization);
|
||||||
}
|
}
|
||||||
@@ -980,6 +980,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!this.organization.permissions.editAnyCollection &&
|
||||||
this.flexibleCollectionsV1Enabled &&
|
this.flexibleCollectionsV1Enabled &&
|
||||||
!c.edit &&
|
!c.edit &&
|
||||||
!this.organization.allowAdminAccessToAllCollectionItems
|
!this.organization.allowAdminAccessToAllCollectionItems
|
||||||
@@ -992,8 +993,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow restore of an Unassigned Item
|
||||||
try {
|
try {
|
||||||
const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled);
|
const asAdmin =
|
||||||
|
this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
|
||||||
|
c.isUnassigned;
|
||||||
await this.cipherService.restoreWithServer(c.id, asAdmin);
|
await this.cipherService.restoreWithServer(c.id, asAdmin);
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
||||||
this.refresh();
|
this.refresh();
|
||||||
@@ -1004,6 +1008,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async bulkRestore(ciphers: CipherView[]) {
|
async bulkRestore(ciphers: CipherView[]) {
|
||||||
if (
|
if (
|
||||||
|
!this.organization.permissions.editAnyCollection &&
|
||||||
this.flexibleCollectionsV1Enabled &&
|
this.flexibleCollectionsV1Enabled &&
|
||||||
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
|
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
|
||||||
) {
|
) {
|
||||||
@@ -1015,13 +1020,46 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
// assess if there are unassigned ciphers and/or editable ciphers selected in bulk for restore
|
||||||
if (selectedCipherIds.length === 0) {
|
const editAccessCiphers: string[] = [];
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
const unassignedCiphers: string[] = [];
|
||||||
|
|
||||||
|
// If user has edit all Access no need to check for unassigned ciphers
|
||||||
|
const canEditAll = this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (canEditAll) {
|
||||||
|
ciphers.map((cipher) => {
|
||||||
|
editAccessCiphers.push(cipher.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ciphers.map((cipher) => {
|
||||||
|
if (cipher.collectionIds.length === 0) {
|
||||||
|
unassignedCiphers.push(cipher.id);
|
||||||
|
} else if (cipher.edit) {
|
||||||
|
editAccessCiphers.push(cipher.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unassignedCiphers.length === 0 && editAccessCiphers.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("nothingSelected"),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.cipherService.restoreManyWithServer(selectedCipherIds);
|
if (unassignedCiphers.length > 0 || editAccessCiphers.length > 0) {
|
||||||
|
await this.cipherService.restoreManyWithServer(
|
||||||
|
[...unassignedCiphers, ...editAccessCiphers],
|
||||||
|
this.organization.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -1030,7 +1068,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
if (
|
if (
|
||||||
this.flexibleCollectionsV1Enabled &&
|
this.flexibleCollectionsV1Enabled &&
|
||||||
!c.edit &&
|
!c.edit &&
|
||||||
!this.organization.allowAdminAccessToAllCollectionItems
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
this.showMissingPermissionsError();
|
this.showMissingPermissionsError();
|
||||||
return;
|
return;
|
||||||
@@ -1053,7 +1094,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.deleteCipherWithServer(c.id, permanent);
|
await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned);
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
@@ -1065,7 +1106,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
async deleteCollection(collection: CollectionAdminView): Promise<void> {
|
||||||
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
|
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
|
||||||
this.showMissingPermissionsError();
|
this.showMissingPermissionsError();
|
||||||
return;
|
return;
|
||||||
@@ -1111,6 +1152,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow bulk deleting of Unassigned Items
|
||||||
|
const unassignedCiphers: string[] = [];
|
||||||
|
const assignedCiphers: string[] = [];
|
||||||
|
|
||||||
|
ciphers.map((c) => {
|
||||||
|
if (c.isUnassigned) {
|
||||||
|
unassignedCiphers.push(c.id);
|
||||||
|
} else {
|
||||||
|
assignedCiphers.push(c.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (ciphers.length === 0 && collections.length === 0) {
|
if (ciphers.length === 0 && collections.length === 0) {
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
return;
|
return;
|
||||||
@@ -1121,8 +1174,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
|
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
|
||||||
const canDeleteCiphers =
|
const canDeleteCiphers =
|
||||||
ciphers == null ||
|
ciphers == null ||
|
||||||
this.organization.allowAdminAccessToAllCollectionItems ||
|
ciphers.every((c) => c.edit) ||
|
||||||
ciphers.every((c) => c.edit);
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
|
if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
|
||||||
this.showMissingPermissionsError();
|
this.showMissingPermissionsError();
|
||||||
@@ -1132,9 +1188,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
const dialog = openBulkDeleteDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
permanent: this.filter.type === "trash",
|
permanent: this.filter.type === "trash",
|
||||||
cipherIds: ciphers.map((c) => c.id),
|
cipherIds: assignedCiphers,
|
||||||
collections: collections,
|
collections: collections,
|
||||||
organization,
|
organization,
|
||||||
|
unassignedCiphers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1331,11 +1388,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) {
|
||||||
const asAdmin = this.organization?.canEditAllCiphers(
|
const asAdmin =
|
||||||
|
this.organization?.canEditAllCiphers(
|
||||||
this.flexibleCollectionsV1Enabled,
|
this.flexibleCollectionsV1Enabled,
|
||||||
this.restrictProviderAccessEnabled,
|
this.restrictProviderAccessEnabled,
|
||||||
);
|
) || isUnassigned;
|
||||||
return permanent
|
return permanent
|
||||||
? this.cipherService.deleteWithServer(id, asAdmin)
|
? this.cipherService.deleteWithServer(id, asAdmin)
|
||||||
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ config.content = [
|
|||||||
"./src/**/*.{html,ts}",
|
"./src/**/*.{html,ts}",
|
||||||
"../../libs/components/src/**/*.{html,ts}",
|
"../../libs/components/src/**/*.{html,ts}",
|
||||||
"../../libs/auth/src/**/*.{html,ts}",
|
"../../libs/auth/src/**/*.{html,ts}",
|
||||||
|
"../../libs/angular/src/**/*.{html,ts}",
|
||||||
"../../bitwarden_license/bit-web/src/**/*.{html,ts}",
|
"../../bitwarden_license/bit-web/src/**/*.{html,ts}",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<bit-form-control>
|
<bit-form-control>
|
||||||
<input type="checkbox" bitCheckbox [formControl]="enabled" />
|
<input type="checkbox" id="enabled" bitCheckbox [formControl]="enabled" />
|
||||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<div class="icon" aria-hidden="true">
|
<div class="tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<img
|
<img
|
||||||
[src]="data.image"
|
[src]="data.image"
|
||||||
[appFallbackSrc]="data.fallbackImage"
|
[appFallbackSrc]="data.fallbackImage"
|
||||||
*ngIf="data.imageEnabled && data.image"
|
*ngIf="data.imageEnabled && data.image"
|
||||||
|
class="tw-max-h-6 tw-max-w-6 tw-rounded-md"
|
||||||
alt=""
|
alt=""
|
||||||
decoding="async"
|
decoding="async"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ import {
|
|||||||
CollectionDetailsResponse,
|
CollectionDetailsResponse,
|
||||||
CollectionResponse,
|
CollectionResponse,
|
||||||
} from "../vault/models/response/collection.response";
|
} from "../vault/models/response/collection.response";
|
||||||
|
import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response";
|
||||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,7 +219,10 @@ export abstract class ApiService {
|
|||||||
putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>;
|
putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>;
|
||||||
putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>;
|
putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>;
|
||||||
putShareCiphers: (request: CipherBulkShareRequest) => Promise<any>;
|
putShareCiphers: (request: CipherBulkShareRequest) => Promise<any>;
|
||||||
putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise<CipherResponse>;
|
putCipherCollections: (
|
||||||
|
id: string,
|
||||||
|
request: CipherCollectionsRequest,
|
||||||
|
) => Promise<OptionalCipherResponse>;
|
||||||
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
|
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
|
||||||
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
|
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
|
||||||
putDeleteCipher: (id: string) => Promise<any>;
|
putDeleteCipher: (id: string) => Promise<any>;
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ import {
|
|||||||
CollectionDetailsResponse,
|
CollectionDetailsResponse,
|
||||||
CollectionResponse,
|
CollectionResponse,
|
||||||
} from "../vault/models/response/collection.response";
|
} from "../vault/models/response/collection.response";
|
||||||
|
import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response";
|
||||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -566,9 +567,15 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
async putCipherCollections(
|
async putCipherCollections(
|
||||||
id: string,
|
id: string,
|
||||||
request: CipherCollectionsRequest,
|
request: CipherCollectionsRequest,
|
||||||
): Promise<CipherResponse> {
|
): Promise<OptionalCipherResponse> {
|
||||||
const response = await this.send("PUT", "/ciphers/" + id + "/collections", request, true, true);
|
const response = await this.send(
|
||||||
return new CipherResponse(response);
|
"PUT",
|
||||||
|
"/ciphers/" + id + "/collections_v2",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new OptionalCipherResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> {
|
putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { FieldView } from "../models/view/field.view";
|
|||||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||||
|
|
||||||
export abstract class CipherService {
|
export abstract class CipherService {
|
||||||
|
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
||||||
/**
|
/**
|
||||||
* An observable monitoring the add/edit cipher info saved to memory.
|
* An observable monitoring the add/edit cipher info saved to memory.
|
||||||
*/
|
*/
|
||||||
@@ -34,6 +35,12 @@ export abstract class CipherService {
|
|||||||
includeOtherTypes?: CipherType[],
|
includeOtherTypes?: CipherType[],
|
||||||
defaultMatch?: UriMatchStrategySetting,
|
defaultMatch?: UriMatchStrategySetting,
|
||||||
) => Promise<CipherView[]>;
|
) => Promise<CipherView[]>;
|
||||||
|
filterCiphersForUrl: (
|
||||||
|
ciphers: CipherView[],
|
||||||
|
url: string,
|
||||||
|
includeOtherTypes?: CipherType[],
|
||||||
|
defaultMatch?: UriMatchStrategySetting,
|
||||||
|
) => Promise<CipherView[]>;
|
||||||
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||||
/**
|
/**
|
||||||
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
|
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
|
||||||
@@ -132,11 +139,7 @@ export abstract class CipherService {
|
|||||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||||
restoreManyWithServer: (
|
restoreManyWithServer: (ids: string[], orgId?: string) => Promise<void>;
|
||||||
ids: string[],
|
|
||||||
organizationId?: string,
|
|
||||||
asAdmin?: boolean,
|
|
||||||
) => Promise<void>;
|
|
||||||
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
|
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
|
||||||
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
|
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
|
import { CipherResponse } from "./cipher.response";
|
||||||
|
|
||||||
|
export class OptionalCipherResponse extends BaseResponse {
|
||||||
|
unavailable: boolean;
|
||||||
|
cipher?: CipherResponse;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.unavailable = this.getResponseProperty("Unavailable");
|
||||||
|
this.cipher = new CipherResponse(this.getResponseProperty("Cipher"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,6 +126,12 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
return this.item?.linkedFieldOptions;
|
return this.item?.linkedFieldOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUnassigned(): boolean {
|
||||||
|
return (
|
||||||
|
this.organizationId != null && (this.collectionIds == null || this.collectionIds.length === 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
linkedFieldValue(id: LinkedIdType) {
|
linkedFieldValue(id: LinkedIdType) {
|
||||||
const linkedFieldOption = this.linkedFieldOptions?.get(id);
|
const linkedFieldOption = this.linkedFieldOptions?.get(id);
|
||||||
if (linkedFieldOption == null) {
|
if (linkedFieldOption == null) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs";
|
import { firstValueFrom, map, Observable, share, skipWhile, switchMap } from "rxjs";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
StateProvider,
|
StateProvider,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||||
import { UserKey, OrgKey } from "../../types/key";
|
import { OrgKey, UserKey } from "../../types/key";
|
||||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||||
import { FieldType } from "../enums";
|
import { FieldType } from "../enums";
|
||||||
@@ -65,10 +65,10 @@ import { PasswordHistoryView } from "../models/view/password-history.view";
|
|||||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ENCRYPTED_CIPHERS,
|
|
||||||
LOCAL_DATA_KEY,
|
|
||||||
ADD_EDIT_CIPHER_INFO_KEY,
|
ADD_EDIT_CIPHER_INFO_KEY,
|
||||||
DECRYPTED_CIPHERS,
|
DECRYPTED_CIPHERS,
|
||||||
|
ENCRYPTED_CIPHERS,
|
||||||
|
LOCAL_DATA_KEY,
|
||||||
} from "./key-state/ciphers.state";
|
} from "./key-state/ciphers.state";
|
||||||
|
|
||||||
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
|
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
|
||||||
@@ -443,15 +443,24 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
url: string,
|
url: string,
|
||||||
includeOtherTypes?: CipherType[],
|
includeOtherTypes?: CipherType[],
|
||||||
defaultMatch: UriMatchStrategySetting = null,
|
defaultMatch: UriMatchStrategySetting = null,
|
||||||
|
): Promise<CipherView[]> {
|
||||||
|
const ciphers = await this.getAllDecrypted();
|
||||||
|
return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterCiphersForUrl(
|
||||||
|
ciphers: CipherView[],
|
||||||
|
url: string,
|
||||||
|
includeOtherTypes?: CipherType[],
|
||||||
|
defaultMatch: UriMatchStrategySetting = null,
|
||||||
): Promise<CipherView[]> {
|
): Promise<CipherView[]> {
|
||||||
if (url == null && includeOtherTypes == null) {
|
if (url == null && includeOtherTypes == null) {
|
||||||
return Promise.resolve([]);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const equivalentDomains = await firstValueFrom(
|
const equivalentDomains = await firstValueFrom(
|
||||||
this.domainSettingsService.getUrlEquivalentDomains(url),
|
this.domainSettingsService.getUrlEquivalentDomains(url),
|
||||||
);
|
);
|
||||||
const ciphers = await this.getAllDecrypted();
|
|
||||||
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
||||||
|
|
||||||
return ciphers.filter((cipher) => {
|
return ciphers.filter((cipher) => {
|
||||||
@@ -776,9 +785,14 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
async saveCollectionsWithServer(cipher: Cipher): Promise<Cipher> {
|
async saveCollectionsWithServer(cipher: Cipher): Promise<Cipher> {
|
||||||
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
||||||
const response = await this.apiService.putCipherCollections(cipher.id, request);
|
const response = await this.apiService.putCipherCollections(cipher.id, request);
|
||||||
const data = new CipherData(response);
|
// The response will now check for an unavailable value. This value determines whether
|
||||||
|
// the user still has Can Manage access to the item after updating.
|
||||||
|
if (response.unavailable) {
|
||||||
|
await this.delete(cipher.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = new CipherData(response.cipher);
|
||||||
const updated = await this.upsert(data);
|
const updated = await this.upsert(data);
|
||||||
// Collection updates don't change local data
|
|
||||||
return new Cipher(updated[cipher.id as CipherId], cipher.localData);
|
return new Cipher(updated[cipher.id as CipherId], cipher.localData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,14 +1131,15 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
await this.restore({ id: id, revisionDate: response.revisionDate });
|
await this.restore({ id: id, revisionDate: response.revisionDate });
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreManyWithServer(
|
/**
|
||||||
ids: string[],
|
* No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
|
||||||
organizationId: string = null,
|
* The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
|
||||||
asAdmin = false,
|
*/
|
||||||
): Promise<void> {
|
async restoreManyWithServer(ids: string[], orgId: string = null): Promise<void> {
|
||||||
let response;
|
let response;
|
||||||
if (asAdmin) {
|
|
||||||
const request = new CipherBulkRestoreRequest(ids, organizationId);
|
if (orgId) {
|
||||||
|
const request = new CipherBulkRestoreRequest(ids, orgId);
|
||||||
response = await this.apiService.putRestoreManyCiphersAdmin(request);
|
response = await this.apiService.putRestoreManyCiphersAdmin(request);
|
||||||
} else {
|
} else {
|
||||||
const request = new CipherBulkRestoreRequest(ids);
|
const request = new CipherBulkRestoreRequest(ids);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./search";
|
export * from "./search";
|
||||||
export * from "./no-access";
|
export * from "./no-access";
|
||||||
|
export * from "./vault";
|
||||||
|
|||||||
17
libs/components/src/icon/icons/vault.ts
Normal file
17
libs/components/src/icon/icons/vault.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { svgIcon } from "../icon";
|
||||||
|
|
||||||
|
export const Vault = svgIcon`
|
||||||
|
<svg fill="none" width="100" height="90" viewBox="0 0 100 90" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path d="m73.446 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7615 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
|
||||||
|
<path d="m10.108 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7614 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
|
||||||
|
<path d="m4.2281 2.4304c-1.1655 0-2.1208 0.95235-2.1208 2.1402v74.318c0 1.1878 0.95534 2.1402 2.1208 2.1402h91.544c1.1655 0 2.1208-0.9524 2.1208-2.1402v-74.318c0-1.1878-0.9553-2.1402-2.1208-2.1402h-91.544zm-4.1208 2.1402c0-2.2807 1.8391-4.1402 4.1208-4.1402h91.544c2.2817 0 4.1208 1.8595 4.1208 4.1402v74.318c0 2.2807-1.8391 4.1402-4.1208 4.1402h-91.544c-2.2817 0-4.1208-1.8595-4.1208-4.1402v-74.318z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
|
||||||
|
<path d="m89.258 21.816c-0.7304 0-1.3307 0.5963-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7458-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
|
||||||
|
<path d="m89.258 45.237c-0.7304 0-1.3307 0.5962-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7459-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
|
||||||
|
<path d="m33.443 25.468c0-0.5523 0.4477-1 1-1 1.4163 0 2.6668 1.0953 2.6668 2.5705v21.595c0 1.4752-1.2505 2.5705-2.6668 2.5705-0.5523 0-1-0.4477-1-1s0.4477-1 1-1c0.4255 0 0.6668-0.3103 0.6668-0.5705v-21.595c0-0.2602-0.2413-0.5705-0.6668-0.5705-0.5523 0-1-0.4477-1-1z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
|
||||||
|
<path d="m60.556 48.551c-3.2028 0-5.7978-3.1022-5.7978-6.9179 0-3.8156 2.595-6.9114 5.7978-6.9114 3.2029 0 5.7913 3.1022 5.7913 6.9114 0 3.8093-2.5949 6.9179-5.7913 6.9179zm0-14.791c-3.6408 0-6.6018 3.529-6.6018 7.8733 0 4.3444 2.961 7.8798 6.6018 7.8798s6.5953-3.529 6.5953-7.8798c0-4.3507-2.961-7.8733-6.5953-7.8733z" class="tw-fill-info-600"/>
|
||||||
|
<path d="m60.556 26.027c-0.4379 0-0.804 0.4267-0.804 0.9555l-0.0201 3.257c-2.0247 0.2075-3.8681 1.1748-5.3381 2.6521l-1.9561-2.2909c-0.156-0.1901-0.3638-0.2856-0.5654-0.2866h0.0033-0.0065 0.0032c-0.2015 1e-3 -0.4028 0.0965-0.5588 0.2866-0.3138 0.3695-0.3138 0.9746 0 1.3441l1.9348 2.3123 0.0034 0.0042c-1.2532 1.7574-2.0625 3.9789-2.2323 6.4166h0.7647c0.0488 0 0.0966 0.0053 0.143 0.0154-0.0465-0.01-0.0942-0.0152-0.143-0.0152h-3.497c-0.438 0-0.804 0.4268-0.804 0.9491 0 0.5224 0.366 0.9555 0.804 0.9555h2.7323c0.1698 2.4381 0.986 4.66 2.2331 6.4175l-0.0028 0.0034-1.9297 2.3187c-0.3138 0.3694-0.3138 0.9746 0 1.344 0.1568 0.1848 0.3595 0.2803 0.5621 0.2803s0.4118-0.0955 0.5687-0.2803l1.9282-2.3123 1e-4 -1e-4c1.4757 1.4954 3.3361 2.4695 5.3729 2.6684v3.2622c0 0.5287 0.3661 0.9555 0.804 0.9555 0.438 0 0.7975-0.4268 0.7975-0.9555l0.0212-3.263c2.0293-0.2066 3.8833-1.1701 5.3555-2.6581l0.0028 0.0033 1.9282 2.306c0.1569 0.1847 0.3661 0.2803 0.5687 0.2803s0.4118-0.0956 0.5687-0.2803c0.3137-0.3695 0.3137-0.9746 0-1.3441l-1.9269-2.3334c1.2466-1.7626 2.0628-3.9762 2.2337-6.4125h2.7195c0.438 0 0.804-0.4268 0.804-0.9555s-0.366-0.9491-0.804-0.9491l-2.7198-0.0166c-0.1709-2.4276-0.9825-4.634-2.2222-6.3932l1.9157-2.3235c0.3137-0.3695 0.3137-0.9746 0-1.3441-0.1569-0.1911-0.3661-0.2866-0.5687-0.2866s-0.4118 0.0955-0.5687 0.2866l-1.9222 2.2988c-1.4756-1.4884-3.3591-2.454-5.3855-2.665v-3.252c0-0.5288-0.353-0.9555-0.7975-0.9555zm6.72 8.9311c0.0201-0.02 0.0396-0.0413 0.0584-0.0642l0.0144-0.0173-0.021 0.0239c-0.0167 0.0203-0.034 0.0395-0.0518 0.0576zm1.2545 6.6691c-0.0028-0.0609-0.0032-0.1186-0.0013-0.1732-0.0775-5.1648-3.6205-9.3364-7.9594-9.3438l-0.0138 1e-4 -0.0114-1e-4c-4.3862 0.0089-7.9565 4.2734-7.9565 9.5168 0 5.2239 3.5479 9.4824 7.9117 9.5229 0.0178-9e-4 0.036-0.0014 0.0546-0.0014 0.0194 0 0.0385 5e-4 0.0572 0.0015 4.3591-0.0317 7.9116-4.2849 7.9189-9.5068v-0.016zm-13.411 7.6696c0.0205-0.0858 0.0307-0.174 0.0307-0.2615 0-0.242-0.0784-0.4904-0.2353-0.6752-0.1503-0.1911-0.3595-0.2803-0.5621-0.2803l-0.0114 1e-4h0.0113c0.2026 0 0.4118 0.0892 0.5621 0.2803 0.1569 0.1847 0.2353 0.4332 0.2353 0.6752 0 0.0874-0.0102 0.1757-0.0306 0.2614zm-2.5382-7.6696c0-0.0236-7e-4 -0.0471-0.0021-0.0702 0.0014 0.0231 0.0022 0.0465 0.0022 0.07 0 0.0175-4e-4 0.0349-0.0012 0.0521 7e-4 -0.0172 0.0011-0.0345 0.0011-0.0519z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
|
||||||
|
<path d="m25.442 10.125c0-1.2133 1.0146-2.1704 2.2154-2.1008l58.262 3.4199c1.1054 0.0669 1.9723 0.9842 1.9723 2.1009v7.3296h2v-7.3296c0-2.1736-1.6899-3.9673-3.853-4.0974l-58.264-3.42c-2.1001-0.12216-3.8976 1.356-4.264 3.347h-8.7578c-2.2641 0-4.0891 1.845-4.0891 4.1081v55.945c0 2.2631 1.825 4.1081 4.0891 4.1081h8.7036c0.1798 2.1936 2.0771 3.8865 4.3187 3.7561l58.264-3.4201c2.1631-0.1301 3.853-1.9237 3.853-4.0973v-11.184h-2v11.184c0 1.117-0.8674 2.0344-1.9731 2.1009l-58.261 3.4199c-1.2008 0.0696-2.2155-0.8875-2.2155-2.1009v-63.07zm-2 61.411v-60.162h-8.6897c-1.148 0-2.0891 0.9381-2.0891 2.1081v55.945c0 1.1701 0.9411 2.1081 2.0891 2.1081h8.6897zm64.449-36.67v9.4289h2v-9.4289h-2z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
@@ -60,6 +60,7 @@ module.exports = {
|
|||||||
contrast: rgba("--color-text-contrast"),
|
contrast: rgba("--color-text-contrast"),
|
||||||
alt2: rgba("--color-text-alt2"),
|
alt2: rgba("--color-text-alt2"),
|
||||||
code: rgba("--color-text-code"),
|
code: rgba("--color-text-code"),
|
||||||
|
headers: rgba("--color-text-headers"),
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
DEFAULT: rgba("--color-background"),
|
DEFAULT: rgba("--color-background"),
|
||||||
|
|||||||
Reference in New Issue
Block a user