mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-29 22:31:29 +01:00
Merge branch 'main' into PM-12036
This commit is contained in:
commit
ced9b7c137
@ -1,29 +0,0 @@
|
||||
**/build
|
||||
**/dist
|
||||
**/coverage
|
||||
.angular
|
||||
storybook-static
|
||||
|
||||
**/node_modules
|
||||
|
||||
**/webpack.*.js
|
||||
**/jest.config.js
|
||||
|
||||
apps/browser/config/config.js
|
||||
apps/browser/src/auth/scripts/duo.js
|
||||
apps/browser/webpack/manifest.js
|
||||
|
||||
apps/desktop/desktop_native
|
||||
apps/desktop/src/auth/scripts/duo.js
|
||||
|
||||
apps/web/config.js
|
||||
apps/web/scripts/*.js
|
||||
apps/web/tailwind.config.js
|
||||
|
||||
apps/cli/config/config.js
|
||||
|
||||
tailwind.config.js
|
||||
libs/components/tailwind.config.base.js
|
||||
libs/components/tailwind.config.js
|
||||
|
||||
scripts/*.js
|
258
.eslintrc.json
258
.eslintrc.json
@ -1,258 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.js"],
|
||||
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.eslint.json"],
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:rxjs/recommended",
|
||||
"prettier",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@angular-eslint/component-class-suffix": 0,
|
||||
"@angular-eslint/contextual-lifecycle": 0,
|
||||
"@angular-eslint/directive-class-suffix": 0,
|
||||
"@angular-eslint/no-empty-lifecycle-method": 0,
|
||||
"@angular-eslint/no-host-metadata-property": 0,
|
||||
"@angular-eslint/no-input-rename": 0,
|
||||
"@angular-eslint/no-inputs-metadata-property": 0,
|
||||
"@angular-eslint/no-output-native": 0,
|
||||
"@angular-eslint/no-output-on-prefix": 0,
|
||||
"@angular-eslint/no-output-rename": 0,
|
||||
"@angular-eslint/no-outputs-metadata-property": 0,
|
||||
"@angular-eslint/use-lifecycle-interface": "error",
|
||||
"@angular-eslint/use-pipe-transform-interface": 0,
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{ "accessibility": "no-public" }
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
|
||||
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
|
||||
"@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"no-console": "error",
|
||||
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@bitwarden/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||
}
|
||||
],
|
||||
"rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }],
|
||||
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"message": "Calling `svgIcon` directly is not allowed",
|
||||
"selector": "CallExpression[callee.name='svgIcon']"
|
||||
},
|
||||
{
|
||||
"message": "Accessing FormGroup using `get` is not allowed, use `.value` instead",
|
||||
"selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']"
|
||||
}
|
||||
],
|
||||
"curly": ["error", "all"],
|
||||
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
|
||||
"import/no-restricted-paths": [
|
||||
"error",
|
||||
{
|
||||
"zones": [
|
||||
{
|
||||
"target": ["libs/**/*"],
|
||||
"from": ["apps/**/*"],
|
||||
"message": "Libs should not import app-specific code."
|
||||
},
|
||||
{
|
||||
// avoid specific frameworks or large dependencies in common
|
||||
"target": "./libs/common/**/*",
|
||||
"from": [
|
||||
// Angular
|
||||
"./libs/angular/**/*",
|
||||
"./node_modules/@angular*/**/*",
|
||||
|
||||
// Node
|
||||
"./libs/node/**/*",
|
||||
|
||||
//Generator
|
||||
"./libs/tools/generator/components/**/*",
|
||||
"./libs/tools/generator/core/**/*",
|
||||
"./libs/tools/generator/extensions/**/*",
|
||||
|
||||
// Import/export
|
||||
"./libs/importer/**/*",
|
||||
"./libs/tools/export/vault-export/vault-export-core/**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
// avoid import of unexported state objects
|
||||
"target": [
|
||||
"!(libs)/**/*",
|
||||
"libs/!(common)/**/*",
|
||||
"libs/common/!(src)/**/*",
|
||||
"libs/common/src/!(platform)/**/*",
|
||||
"libs/common/src/platform/!(state)/**/*"
|
||||
],
|
||||
"from": ["./libs/common/src/platform/state/**/*"],
|
||||
// allow module index import
|
||||
"except": ["**/state/index.ts"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"parser": "@angular-eslint/template-parser",
|
||||
"plugins": ["@angular-eslint/template", "tailwindcss"],
|
||||
"rules": {
|
||||
"@angular-eslint/template/button-has-type": "error",
|
||||
"tailwindcss/no-custom-classname": [
|
||||
"error",
|
||||
{
|
||||
// uses negative lookahead to whitelist any class that doesn't start with "tw-"
|
||||
// in other words: classnames that start with tw- must be valid TailwindCSS classes
|
||||
"whitelist": ["(?!(tw)\\-).*"]
|
||||
}
|
||||
],
|
||||
"tailwindcss/enforces-negative-arbitrary-values": "error",
|
||||
"tailwindcss/enforces-shorthand": "error",
|
||||
"tailwindcss/no-contradicting-classname": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"],
|
||||
"excludedFiles": [
|
||||
"apps/browser/src/autofill/{content,notification}/**/*.ts",
|
||||
"apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background
|
||||
"apps/browser/src/platform/background.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead",
|
||||
// This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage
|
||||
"selector": "CallExpression > [object.object.object.name='chrome'][property.name='addListener']"
|
||||
},
|
||||
{
|
||||
"message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead",
|
||||
// This selector covers events like chrome.storage.local.onChange
|
||||
"selector": "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"excludedFiles": ["**/platform/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"**/platform/**/internal", // General internal pattern
|
||||
// All features that have been converted to barrel files
|
||||
"**/platform/messaging/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/src/**/*.ts"],
|
||||
"excludedFiles": ["**/platform/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"**/platform/**/internal", // General internal pattern
|
||||
// All features that have been converted to barrel files
|
||||
"**/platform/messaging/**",
|
||||
"**/src/**/*" // Prevent relative imports across libs.
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["bitwarden_license/bit-common/src/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{ "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["apps/**/*.ts"],
|
||||
"rules": {
|
||||
// Catches static imports
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"biwarden_license/**",
|
||||
"@bitwarden/bit-common/*",
|
||||
"@bitwarden/bit-web/*",
|
||||
"**/src/**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
// Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"message": "Don't import Bitwarden licensed code into OSS code.",
|
||||
"selector": "ImportExpression > Literal.source[value=/.*(bitwarden_license|bit-common|bit-web).*/]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
25
.github/renovate.json
vendored
25
.github/renovate.json
vendored
@ -69,6 +69,26 @@
|
||||
"commitMessagePrefix": "[deps] Auth:",
|
||||
"reviewers": ["team:team-auth-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"@angular-eslint/schematics",
|
||||
"angular-eslint",
|
||||
"eslint-config-prettier",
|
||||
"eslint-import-resolver-typescript",
|
||||
"eslint-plugin-import",
|
||||
"eslint-plugin-rxjs-angular",
|
||||
"eslint-plugin-rxjs",
|
||||
"eslint-plugin-storybook",
|
||||
"eslint-plugin-tailwindcss",
|
||||
"eslint",
|
||||
"husky",
|
||||
"lint-staged",
|
||||
"typescript-eslint"
|
||||
],
|
||||
"description": "Architecture owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Architecture:",
|
||||
"reviewers": ["team:dept-architecture"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"@angular-eslint/eslint-plugin-template",
|
||||
@ -88,9 +108,8 @@
|
||||
"husky",
|
||||
"lint-staged"
|
||||
],
|
||||
"description": "Architecture owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Architecture:",
|
||||
"reviewers": ["team:dept-architecture"]
|
||||
"groupName": "Linting minor-patch",
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { StorybookConfig } from "@storybook/angular";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
@ -29,6 +30,8 @@ const config: StorybookConfig = {
|
||||
getAbsolutePath("@storybook/addon-designs"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
{
|
||||
// @storybook/addon-docs is part of @storybook/addon-essentials
|
||||
// eslint-disable-next-line storybook/no-uninstalled-addons
|
||||
name: "@storybook/addon-docs",
|
||||
options: {
|
||||
mdxPluginOptions: {
|
||||
|
@ -50,10 +50,14 @@ const darkTheme = create({
|
||||
});
|
||||
|
||||
export const getPreferredColorScheme = () => {
|
||||
if (!globalThis || !globalThis.matchMedia) return "light";
|
||||
if (!globalThis || !globalThis.matchMedia) {
|
||||
return "light";
|
||||
}
|
||||
|
||||
const isDarkThemePreferred = globalThis.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (isDarkThemePreferred) return "dark";
|
||||
if (isDarkThemePreferred) {
|
||||
return "dark";
|
||||
}
|
||||
|
||||
return "light";
|
||||
};
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -7,5 +7,6 @@
|
||||
"**/_locales/*[^n]/messages.json": true
|
||||
},
|
||||
"rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"eslint.useFlatConfig": true
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/**/*.ts"],
|
||||
"excludedFiles": [
|
||||
"src/**/{content,popup,spec}/**/*.ts",
|
||||
"src/**/autofill/{notification,overlay}/**/*.ts",
|
||||
"src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts",
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "window",
|
||||
"message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -13,9 +13,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
@ -34,9 +32,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
apiService: ApiService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
auditService: AuditService,
|
||||
@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
i18nService,
|
||||
keyService,
|
||||
apiService,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { dirname, join } from "path";
|
||||
import path from "path";
|
||||
import path, { dirname, join } from "path";
|
||||
|
||||
import type { StorybookConfig } from "@storybook/web-components-webpack5";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
|
||||
const getAbsolutePath = (value: string): string =>
|
||||
dirname(require.resolve(join(value, "package.json")));
|
||||
|
@ -47,6 +47,6 @@
|
||||
|
||||
<div
|
||||
*ngIf="showOverlay"
|
||||
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||
></div>
|
||||
</ng-container>
|
||||
|
@ -13,13 +13,13 @@
|
||||
<ng-content select="[slot=above-scroll-area]"></ng-content>
|
||||
</div>
|
||||
<div
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-size-full tw-styled-scrollbar"
|
||||
data-testid="popup-layout-scroll-region"
|
||||
(scroll)="handleScroll($event)"
|
||||
[ngClass]="{ 'tw-invisible': loading }"
|
||||
>
|
||||
<div
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-size-full"
|
||||
[ngClass]="{ 'tw-p-3 bit-compact:tw-p-2': !disablePadding }"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
|
@ -11,11 +11,11 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { DialogService, ItemModule } from "@bitwarden/components";
|
||||
|
||||
import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
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 { FamiliesPolicyService } from "../../../../services/families-policy.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "more-from-bitwarden-page-v2.component.html",
|
||||
|
@ -20,7 +20,7 @@
|
||||
</p>
|
||||
<div
|
||||
*ngIf="showBadge$ | async"
|
||||
class="tw-flex tw-items-center tw-justify-center tw-z-10 tw-absolute tw-rounded-full tw-h-[15px] tw-w-[15px] tw-top-[1px] tw-right-[1px] tw-text-notification-600 tw-text-[8px] tw-border-notification-600 tw-border-[0.5px] tw-border-solid tw-bg-notification-100 tw-leading-normal"
|
||||
class="tw-flex tw-items-center tw-justify-center tw-z-10 tw-absolute tw-rounded-full tw-size-[15px] tw-top-[1px] tw-right-[1px] tw-text-notification-600 tw-text-[8px] tw-border-notification-600 tw-border-[0.5px] tw-border-solid tw-bg-notification-100 tw-leading-normal"
|
||||
data-testid="filter-badge"
|
||||
>
|
||||
{{ numberOfAppliedFilters$ | async }}
|
||||
|
@ -2,8 +2,9 @@
|
||||
<form
|
||||
[formGroup]="filterForm"
|
||||
class="tw-gap-2 tw-mt-2 tw-grid tw-grid-cols-2 sm:tw-grid-cols-3 lg:tw-grid-cols-4"
|
||||
*ngIf="allFilters$ | async as allFilters"
|
||||
>
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<ng-container *ngIf="allFilters.organizations as organizations">
|
||||
<bit-chip-select
|
||||
*ngIf="organizations.length"
|
||||
fullWidth
|
||||
@ -14,7 +15,7 @@
|
||||
>
|
||||
</bit-chip-select>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="collections$ | async as collections">
|
||||
<ng-container *ngIf="allFilters.collections as collections">
|
||||
<bit-chip-select
|
||||
*ngIf="collections.length"
|
||||
fullWidth
|
||||
@ -25,7 +26,7 @@
|
||||
>
|
||||
</bit-chip-select>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="folders$ | async as folders">
|
||||
<ng-container *ngIf="allFilters.folders as folders">
|
||||
<bit-chip-select
|
||||
*ngIf="folders.length"
|
||||
fullWidth
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { combineLatest, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ChipSelectComponent } from "@bitwarden/components";
|
||||
@ -20,5 +21,20 @@ export class VaultListFiltersComponent {
|
||||
protected folders$ = this.vaultPopupListFiltersService.folders$;
|
||||
protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes;
|
||||
|
||||
// Combine all filters into a single observable to eliminate the filters from loading separately in the UI.
|
||||
protected allFilters$ = combineLatest([
|
||||
this.organizations$,
|
||||
this.collections$,
|
||||
this.folders$,
|
||||
]).pipe(
|
||||
map(([organizations, collections, folders]) => {
|
||||
return {
|
||||
organizations,
|
||||
collections,
|
||||
folders,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CdkVirtualScrollableElement, ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnDestroy, OnInit } from "@angular/core";
|
||||
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { combineLatest, Observable, shareReplay, switchMap } from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
import { combineLatest, filter, map, Observable, shareReplay, switchMap, take } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
@ -19,6 +18,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service";
|
||||
|
||||
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
|
||||
import {
|
||||
@ -58,7 +58,9 @@ enum VaultState {
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
|
||||
|
||||
cipherType = CipherType;
|
||||
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
@ -88,9 +90,12 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
|
||||
protected VaultStateEnum = VaultState;
|
||||
|
||||
private allFilters$ = this.vaultPopupListFiltersService.allFilters$;
|
||||
|
||||
constructor(
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private vaultPopupListFiltersService: VaultPopupListFiltersService,
|
||||
private vaultScrollPositionService: VaultPopupScrollPositionService,
|
||||
private destroyRef: DestroyRef,
|
||||
private cipherService: CipherService,
|
||||
private dialogService: DialogService,
|
||||
@ -119,6 +124,17 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.virtualScrollElement) {
|
||||
// The filters component can cause the size of the virtual scroll element to change,
|
||||
// which can cause the scroll position to be land in the wrong spot. To fix this,
|
||||
// wait until all filters are populated before restoring the scroll position.
|
||||
this.allFilters$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.vaultScrollPositionService.start(this.virtualScrollElement!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.cipherService.failedToDecryptCiphers$
|
||||
.pipe(
|
||||
@ -134,5 +150,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
ngOnDestroy(): void {
|
||||
this.vaultScrollPositionService.stop();
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,15 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CopyCipherFieldService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
|
||||
|
||||
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
|
||||
import { ViewV2Component } from "./view-v2.component";
|
||||
@ -44,6 +47,10 @@ describe("ViewV2Component", () => {
|
||||
const collect = jest.fn().mockResolvedValue(null);
|
||||
const doAutofill = jest.fn().mockResolvedValue(true);
|
||||
const copy = jest.fn().mockResolvedValue(true);
|
||||
const back = jest.fn().mockResolvedValue(null);
|
||||
const openSimpleDialog = jest.fn().mockResolvedValue(true);
|
||||
const stop = jest.fn();
|
||||
const showToast = jest.fn();
|
||||
|
||||
const mockCipher = {
|
||||
id: "122-333-444",
|
||||
@ -54,7 +61,7 @@ describe("ViewV2Component", () => {
|
||||
password: "test-password",
|
||||
totp: "123",
|
||||
},
|
||||
};
|
||||
} as unknown as CipherView;
|
||||
|
||||
const mockVaultPopupAutofillService = {
|
||||
doAutofill,
|
||||
@ -68,13 +75,21 @@ describe("ViewV2Component", () => {
|
||||
const mockCipherService = {
|
||||
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
|
||||
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
||||
deleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCipherService.deleteWithServer.mockClear();
|
||||
mockCipherService.softDeleteWithServer.mockClear();
|
||||
mockNavigate.mockClear();
|
||||
collect.mockClear();
|
||||
doAutofill.mockClear();
|
||||
copy.mockClear();
|
||||
stop.mockClear();
|
||||
openSimpleDialog.mockClear();
|
||||
back.mockClear();
|
||||
showToast.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ViewV2Component],
|
||||
@ -84,9 +99,12 @@ describe("ViewV2Component", () => {
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>({ back }) },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
|
||||
{ provide: EventCollectionService, useValue: { collect } },
|
||||
{ provide: VaultPopupScrollPositionService, useValue: { stop } },
|
||||
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
|
||||
{ provide: ToastService, useValue: { showToast } },
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
@ -98,7 +116,6 @@ describe("ViewV2Component", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
@ -114,7 +131,13 @@ describe("ViewV2Component", () => {
|
||||
useValue: mockCopyCipherFieldService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideProvider(DialogService, {
|
||||
useValue: {
|
||||
openSimpleDialog,
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewV2Component);
|
||||
component = fixture.componentInstance;
|
||||
@ -223,4 +246,130 @@ describe("ViewV2Component", () => {
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
beforeEach(() => {
|
||||
component.cipher = mockCipher;
|
||||
});
|
||||
|
||||
it("opens confirmation modal", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("navigates back", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(back).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("stops scroll position service", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
expect(stop).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
describe("deny confirmation", () => {
|
||||
beforeEach(() => {
|
||||
openSimpleDialog.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("does not delete the cipher", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled();
|
||||
expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not interact with side effects", () => {
|
||||
expect(back).not.toHaveBeenCalled();
|
||||
expect(stop).not.toHaveBeenCalled();
|
||||
expect(showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("accept confirmation", () => {
|
||||
beforeEach(() => {
|
||||
openSimpleDialog.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("soft delete", () => {
|
||||
beforeEach(() => {
|
||||
(mockCipher as any).isDeleted = null;
|
||||
});
|
||||
|
||||
it("opens confirmation dialog", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
|
||||
expect(openSimpleDialog).toHaveBeenCalledWith({
|
||||
content: {
|
||||
key: "deleteItemConfirmation",
|
||||
},
|
||||
title: {
|
||||
key: "deleteItem",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
});
|
||||
|
||||
it("calls soft delete", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(mockCipherService.softDeleteWithServer).toHaveBeenCalled();
|
||||
expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows toast", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "deletedItem",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hard delete", () => {
|
||||
beforeEach(() => {
|
||||
(mockCipher as any).isDeleted = true;
|
||||
});
|
||||
|
||||
it("opens confirmation dialog", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
|
||||
expect(openSimpleDialog).toHaveBeenCalledWith({
|
||||
content: {
|
||||
key: "permanentlyDeleteItemConfirmation",
|
||||
},
|
||||
title: {
|
||||
key: "deleteItem",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
});
|
||||
|
||||
it("calls soft delete", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(mockCipherService.deleteWithServer).toHaveBeenCalled();
|
||||
expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows toast", async () => {
|
||||
await component.delete();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "permanentlyDeletedItem",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -49,6 +49,7 @@ import { PopOutComponent } from "../../../../../platform/popup/components/pop-ou
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service";
|
||||
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
|
||||
import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window";
|
||||
|
||||
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
|
||||
@ -113,6 +114,7 @@ export class ViewV2Component {
|
||||
private popupRouterCacheService: PopupRouterCacheService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
private copyCipherFieldService: CopyCipherFieldService,
|
||||
private popupScrollPositionService: VaultPopupScrollPositionService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
@ -202,6 +204,7 @@ export class ViewV2Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.popupScrollPositionService.stop(true);
|
||||
await this.popupRouterCacheService.back();
|
||||
|
||||
this.toastService.showToast({
|
||||
|
@ -370,6 +370,9 @@ export class VaultPopupListFiltersService {
|
||||
),
|
||||
);
|
||||
|
||||
/** Organizations, collection, folders filters. */
|
||||
allFilters$ = combineLatest([this.organizations$, this.collections$, this.folders$]);
|
||||
|
||||
/** Updates the stored state for filter visibility. */
|
||||
async updateFilterVisibility(isVisible: boolean): Promise<void> {
|
||||
await this.filterVisibilityState.update(() => isVisible);
|
||||
|
@ -0,0 +1,137 @@
|
||||
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
|
||||
import { fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { Subject, Subscription } from "rxjs";
|
||||
|
||||
import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service";
|
||||
|
||||
describe("VaultPopupScrollPositionService", () => {
|
||||
let service: VaultPopupScrollPositionService;
|
||||
const events$ = new Subject();
|
||||
const unsubscribe = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
unsubscribe.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
VaultPopupScrollPositionService,
|
||||
{ provide: Router, useValue: { events: events$ } },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(VaultPopupScrollPositionService);
|
||||
|
||||
// set up dummy values
|
||||
service["scrollPosition"] = 234;
|
||||
service["scrollSubscription"] = { unsubscribe } as unknown as Subscription;
|
||||
});
|
||||
|
||||
describe("router events", () => {
|
||||
it("does not reset service when navigating to `/tabs/vault`", fakeAsync(() => {
|
||||
const event = new NavigationEnd(22, "/tabs/vault", "");
|
||||
events$.next(event);
|
||||
|
||||
tick();
|
||||
|
||||
expect(service["scrollPosition"]).toBe(234);
|
||||
expect(service["scrollSubscription"]).not.toBeNull();
|
||||
}));
|
||||
|
||||
it("resets values when navigating to other tab pages", fakeAsync(() => {
|
||||
const event = new NavigationEnd(23, "/tabs/generator", "");
|
||||
events$.next(event);
|
||||
|
||||
tick();
|
||||
|
||||
expect(service["scrollPosition"]).toBeNull();
|
||||
expect(unsubscribe).toHaveBeenCalled();
|
||||
expect(service["scrollSubscription"]).toBeNull();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
it("removes scroll listener", () => {
|
||||
service.stop();
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(service["scrollSubscription"]).toBeNull();
|
||||
});
|
||||
|
||||
it("resets stored values", () => {
|
||||
service.stop(true);
|
||||
|
||||
expect(service["scrollPosition"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
const elementScrolled$ = new Subject();
|
||||
const focus = jest.fn();
|
||||
const nativeElement = {
|
||||
scrollTop: 0,
|
||||
querySelector: jest.fn(() => ({ focus })),
|
||||
addEventListener: jest.fn(),
|
||||
style: {
|
||||
visibility: "",
|
||||
},
|
||||
};
|
||||
const virtualElement = {
|
||||
elementScrolled: () => elementScrolled$,
|
||||
getElementRef: () => ({ nativeElement }),
|
||||
scrollTo: jest.fn(),
|
||||
} as unknown as CdkVirtualScrollableElement;
|
||||
|
||||
afterEach(() => {
|
||||
// remove the actual subscription created by `.subscribe`
|
||||
service["scrollSubscription"]?.unsubscribe();
|
||||
});
|
||||
|
||||
describe("initial scroll position", () => {
|
||||
beforeEach(() => {
|
||||
(virtualElement.scrollTo as jest.Mock).mockClear();
|
||||
nativeElement.querySelector.mockClear();
|
||||
});
|
||||
|
||||
it("does not scroll when `scrollPosition` is null", () => {
|
||||
service["scrollPosition"] = null;
|
||||
|
||||
service.start(virtualElement);
|
||||
|
||||
expect(virtualElement.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scrolls the virtual element to `scrollPosition`", fakeAsync(() => {
|
||||
service["scrollPosition"] = 500;
|
||||
nativeElement.scrollTop = 500;
|
||||
|
||||
service.start(virtualElement);
|
||||
tick();
|
||||
|
||||
expect(virtualElement.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 500 });
|
||||
}));
|
||||
});
|
||||
|
||||
describe("scroll listener", () => {
|
||||
it("unsubscribes from any existing subscription", () => {
|
||||
service.start(virtualElement);
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("subscribes to `elementScrolled`", fakeAsync(() => {
|
||||
virtualElement.measureScrollOffset = jest.fn(() => 455);
|
||||
|
||||
service.start(virtualElement);
|
||||
|
||||
elementScrolled$.next(null); // first subscription is skipped by `skip(1)`
|
||||
elementScrolled$.next(null);
|
||||
tick();
|
||||
|
||||
expect(virtualElement.measureScrollOffset).toHaveBeenCalledTimes(1);
|
||||
expect(virtualElement.measureScrollOffset).toHaveBeenCalledWith("top");
|
||||
expect(service["scrollPosition"]).toBe(455);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { filter, skip, Subscription } from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultPopupScrollPositionService {
|
||||
private router = inject(Router);
|
||||
|
||||
/** Path of the vault screen */
|
||||
private readonly vaultPath = "/tabs/vault";
|
||||
|
||||
/** Current scroll position relative to the top of the viewport. */
|
||||
private scrollPosition: number | null = null;
|
||||
|
||||
/** Subscription associated with the virtual scroll element. */
|
||||
private scrollSubscription: Subscription | null = null;
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
)
|
||||
.subscribe((event) => {
|
||||
this.resetListenerForNavigation(event);
|
||||
});
|
||||
}
|
||||
|
||||
/** Scrolls the user to the stored scroll position and starts tracking scroll of the page. */
|
||||
start(virtualScrollElement: CdkVirtualScrollableElement) {
|
||||
if (this.hasScrollPosition()) {
|
||||
// Use `setTimeout` to scroll after rendering is complete
|
||||
setTimeout(() => {
|
||||
virtualScrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" });
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollSubscription?.unsubscribe();
|
||||
|
||||
// Skip the first scroll event to avoid settings the scroll from the above `scrollTo` call
|
||||
this.scrollSubscription = virtualScrollElement
|
||||
?.elementScrolled()
|
||||
.pipe(skip(1))
|
||||
.subscribe(() => {
|
||||
const offset = virtualScrollElement.measureScrollOffset("top");
|
||||
this.scrollPosition = offset;
|
||||
});
|
||||
}
|
||||
|
||||
/** Stops the scroll listener from updating the stored location. */
|
||||
stop(reset?: true) {
|
||||
this.scrollSubscription?.unsubscribe();
|
||||
this.scrollSubscription = null;
|
||||
|
||||
if (reset) {
|
||||
this.scrollPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true when a scroll position has been stored. */
|
||||
hasScrollPosition() {
|
||||
return this.scrollPosition !== null;
|
||||
}
|
||||
|
||||
/** Conditionally resets the scroll listeners based on the ending path of the navigation */
|
||||
private resetListenerForNavigation(event: NavigationEnd): void {
|
||||
// The vault page is the target of the scroll listener, return early
|
||||
if (event.url === this.vaultPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other tab pages reset the scroll position
|
||||
if (event.url.startsWith("/tabs/")) {
|
||||
this.stop(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable no-undef, @typescript-eslint/no-require-imports */
|
||||
const config = require("../../libs/components/tailwind.config.base");
|
||||
|
||||
config.content = [
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../../../../libs/admin-console/.eslintrc.json"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { homedir } from "os";
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import "module-alias/register";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
@ -12,9 +12,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
const BroadcasterSubscriptionId = "RegisterComponent";
|
||||
@ -32,9 +30,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
apiService: ApiService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
environmentService: EnvironmentService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit,
|
||||
i18nService,
|
||||
keyService,
|
||||
apiService,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
|
@ -22,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui";
|
||||
@ -148,6 +149,16 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cipher when an attachment is altered.
|
||||
* Note: This only updates the `attachments` and `revisionDate`
|
||||
* properties to ensure any in-progress edits are not lost.
|
||||
*/
|
||||
patchCipherAttachments(cipher: CipherView) {
|
||||
this.cipher.attachments = cipher.attachments;
|
||||
this.cipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
async importSshKeyFromClipboard(password: string = "") {
|
||||
const key = await this.platformUtilsService.readFromClipboard();
|
||||
const parsedKey = await ipc.platform.sshAgent.importKey(key, password);
|
||||
|
@ -159,11 +159,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
await this.vaultFilterComponent.reloadOrganizations();
|
||||
break;
|
||||
case "refreshCiphers":
|
||||
// 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
|
||||
this.vaultItemsComponent.refresh();
|
||||
break;
|
||||
case "modalShown":
|
||||
this.showingModal = true;
|
||||
break;
|
||||
@ -535,9 +530,19 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
childComponent.onUploadedAttachment.subscribe((cipher) => {
|
||||
madeAttachmentChanges = true;
|
||||
// Update the edit component cipher with the updated cipher,
|
||||
// which is needed because the revision date is updated when an attachment is altered
|
||||
this.addEditComponent.patchCipherAttachments(cipher);
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
childComponent.onDeletedAttachment.subscribe((cipher) => {
|
||||
madeAttachmentChanges = true;
|
||||
// Update the edit component cipher with the updated cipher,
|
||||
// which is needed because the revision date is updated when an attachment is altered
|
||||
this.addEditComponent.patchCipherAttachments(cipher);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable no-undef, @typescript-eslint/no-require-imports */
|
||||
const config = require("../../libs/components/tailwind.config.base");
|
||||
|
||||
config.content = [
|
||||
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"**/app/core/*",
|
||||
"**/reports/*",
|
||||
"**/app/shared/*",
|
||||
"**/organizations/settings/*",
|
||||
"**/organizations/policies/*",
|
||||
"@bitwarden/web-vault/*",
|
||||
"src/**/*",
|
||||
"bitwarden_license"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../../../../../libs/admin-console/.eslintrc.json"
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
<div
|
||||
class="tw-flex tw-h-32 tw-bg-secondary-100 tw-items-center tw-justify-center tw-pb-2 tw-px-6 lg:tw-pb-4 lg:tw-px-12"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-h-28 tw-w-28 lg:tw-w-40">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-size-28 lg:tw-w-40">
|
||||
<img
|
||||
#imageEle
|
||||
[src]="image"
|
||||
@ -19,7 +19,7 @@
|
||||
<div class="tw-p-5">
|
||||
<h3 class="tw-text-main tw-text-lg tw-font-semibold">{{ name }}</h3>
|
||||
<a
|
||||
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-w-full after:tw-h-full after:tw-left-0 after:tw-top-0"
|
||||
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-size-full after:tw-left-0 after:tw-top-0"
|
||||
[href]="linkURL"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
|
@ -3,7 +3,6 @@ import { LayoutModule } from "@angular/cdk/layout";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { InfiniteScrollDirective } from "ngx-infinite-scroll";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { CoreModule } from "./core";
|
||||
@ -23,7 +22,6 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
CoreModule,
|
||||
InfiniteScrollDirective,
|
||||
DragDropModule,
|
||||
LayoutModule,
|
||||
OssRoutingModule,
|
||||
|
@ -17,9 +17,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
@ -45,9 +43,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
apiService: ApiService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
@ -64,9 +60,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
|
||||
i18nService,
|
||||
keyService,
|
||||
apiService,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
|
@ -28,7 +28,7 @@
|
||||
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
|
||||
customColorSelected,
|
||||
}"
|
||||
class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
|
||||
class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
|
||||
[style.background-color]="customColor$ | async"
|
||||
>
|
||||
<i
|
||||
@ -37,7 +37,7 @@
|
||||
></i>
|
||||
<input
|
||||
tabindex="-1"
|
||||
class="tw-absolute tw-bottom-0 tw-right-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
|
||||
class="tw-absolute tw-bottom-0 tw-right-0 tw-size-px tw-border-none tw-bg-transparent tw-opacity-0"
|
||||
#colorPicker
|
||||
type="color"
|
||||
[ngModel]="customColor$ | async"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<button
|
||||
[bitPopoverTriggerFor]="infoPopover"
|
||||
type="button"
|
||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-h-4 tw-w-4"
|
||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-size-4"
|
||||
[position]="'right-start'"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
@ -15,7 +15,7 @@
|
||||
</bit-popover>
|
||||
<i
|
||||
*ngIf="asyncActionLoading"
|
||||
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-h-4 tw-w-4"
|
||||
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-size-4"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div class="tw-mb-1 tw-items-center" *ngIf="annualPlan !== null">
|
||||
<label class="tw- tw-block tw-text-main" for="annual">
|
||||
<input
|
||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
||||
class="tw-size-4 tw-align-middle"
|
||||
id="annual"
|
||||
name="cadence"
|
||||
type="radio"
|
||||
@ -34,7 +34,7 @@
|
||||
<div class="tw-mb-1 tw-items-center" *ngIf="monthlyPlan !== null">
|
||||
<label class="tw- tw-block tw-text-main" for="monthly">
|
||||
<input
|
||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
||||
class="tw-size-4 tw-align-middle"
|
||||
id="monthly"
|
||||
name="cadence"
|
||||
type="radio"
|
||||
|
@ -23,7 +23,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { FreeTrial } from "../../../core/types/free-trial";
|
||||
import { TrialFlowService } from "../../services/trial-flow.service";
|
||||
import {
|
||||
AddCreditDialogResult,
|
||||
@ -33,6 +32,7 @@ import {
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustPaymentDialogResultType,
|
||||
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
|
||||
import { FreeTrial } from "../../types/free-trial";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./organization-payment-method.component.html",
|
||||
|
@ -16,11 +16,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { FreeTrial } from "../../core/types/free-trial";
|
||||
import {
|
||||
ChangePlanDialogResultType,
|
||||
openChangePlanDialog,
|
||||
} from "../organizations/change-plan-dialog.component";
|
||||
import { FreeTrial } from "../types/free-trial";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class TrialFlowService {
|
||||
|
@ -24,8 +24,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { FreeTrial } from "../../core/types/free-trial";
|
||||
import { TrialFlowService } from "../services/trial-flow.service";
|
||||
import { FreeTrial } from "../types/free-trial";
|
||||
|
||||
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
|
||||
import {
|
||||
|
@ -25,13 +25,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../../auth/organization-invite/accept-organization.service";
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
SubscriptionProduct,
|
||||
TrialOrganizationType,
|
||||
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { RouterService } from "../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
|
||||
|
||||
export type InitiationPath =
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user