mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -16,13 +16,39 @@
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th>
|
||||
<!-- Organization vault -->
|
||||
<th
|
||||
*ngIf="showAdminActions"
|
||||
bitCell
|
||||
bitSortable="name"
|
||||
[fn]="sortByName"
|
||||
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
|
||||
>
|
||||
{{ "name" | i18n }}
|
||||
</th>
|
||||
<!-- Individual vault -->
|
||||
<th
|
||||
*ngIf="!showAdminActions"
|
||||
bitCell
|
||||
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
|
||||
>
|
||||
{{ "name" | i18n }}
|
||||
</th>
|
||||
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn">
|
||||
<th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
|
||||
{{ "groups" | i18n }}
|
||||
</th>
|
||||
<th
|
||||
bitCell
|
||||
bitSortable="permissions"
|
||||
default="desc"
|
||||
[fn]="sortByPermissions"
|
||||
class="tw-w-2/5"
|
||||
*ngIf="showPermissionsColumn"
|
||||
>
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-12 tw-text-right">
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
import {
|
||||
CollectionPermission,
|
||||
convertToPermission,
|
||||
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemEvent } from "./vault-item-event";
|
||||
|
||||
@@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[75.5px]`;
|
||||
|
||||
const MaxSelectionCount = 500;
|
||||
|
||||
type ItemPermission = CollectionPermission | "NoAccess";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
@@ -333,6 +339,119 @@ export class VaultItemsComponent {
|
||||
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
|
||||
*/
|
||||
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts VaultItems based on group names
|
||||
*/
|
||||
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
if (
|
||||
!(a.collection instanceof CollectionAdminView) &&
|
||||
!(b.collection instanceof CollectionAdminView)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const getFirstGroupName = (collection: CollectionAdminView): string => {
|
||||
if (collection.groups.length > 0) {
|
||||
return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
const aGroupName = getFirstGroupName(a.collection as CollectionAdminView);
|
||||
const bGroupName = getFirstGroupName(b.collection as CollectionAdminView);
|
||||
|
||||
// Collections with groups come before collections without groups.
|
||||
// If a collection has no groups, getFirstGroupName returns null.
|
||||
if (aGroupName === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bGroupName === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return aGroupName.localeCompare(bGroupName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
|
||||
* If permissions are equal, it falls back to sorting by name.
|
||||
*/
|
||||
protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
const getPermissionPriority = (item: VaultItem): number => {
|
||||
const permission = item.collection
|
||||
? this.getCollectionPermission(item.collection)
|
||||
: this.getCipherPermission(item.cipher);
|
||||
|
||||
const priorityMap = {
|
||||
[CollectionPermission.Manage]: 5,
|
||||
[CollectionPermission.Edit]: 4,
|
||||
[CollectionPermission.EditExceptPass]: 3,
|
||||
[CollectionPermission.View]: 2,
|
||||
[CollectionPermission.ViewExceptPass]: 1,
|
||||
NoAccess: 0,
|
||||
};
|
||||
|
||||
return priorityMap[permission] ?? -1;
|
||||
};
|
||||
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
const priorityA = getPermissionPriority(a);
|
||||
const priorityB = getPermissionPriority(b);
|
||||
|
||||
// Higher priority first
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
private compareNames(a: VaultItem, b: VaultItem): number {
|
||||
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
|
||||
return getName(a).localeCompare(getName(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems by prioritizing collections over ciphers.
|
||||
* Collections are always placed before ciphers, regardless of the sorting direction.
|
||||
*/
|
||||
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
|
||||
if (a.collection && !b.collection) {
|
||||
return direction === "asc" ? -1 : 1;
|
||||
}
|
||||
|
||||
if (!a.collection && b.collection) {
|
||||
return direction === "asc" ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private hasPersonalItems(): boolean {
|
||||
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
||||
}
|
||||
@@ -346,4 +465,58 @@ export class VaultItemsComponent {
|
||||
private getUniqueOrganizationIds(): Set<string> {
|
||||
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
|
||||
}
|
||||
|
||||
private getGroupName(groupId: string): string | undefined {
|
||||
return this.allGroups.find((g) => g.id === groupId)?.name;
|
||||
}
|
||||
|
||||
private getCollectionPermission(collection: CollectionView): ItemPermission {
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
|
||||
if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
|
||||
return CollectionPermission.Edit;
|
||||
}
|
||||
|
||||
if (collection.assigned) {
|
||||
return convertToPermission(collection);
|
||||
}
|
||||
|
||||
return "NoAccess";
|
||||
}
|
||||
|
||||
private getCipherPermission(cipher: CipherView): ItemPermission {
|
||||
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
|
||||
return CollectionPermission.Manage;
|
||||
}
|
||||
|
||||
const filteredCollections = this.allCollections?.filter((collection) => {
|
||||
if (collection.assigned) {
|
||||
return cipher.collectionIds.find((id) => {
|
||||
if (collection.id === id) {
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (filteredCollections?.length === 1) {
|
||||
return convertToPermission(filteredCollections[0]);
|
||||
}
|
||||
|
||||
if (filteredCollections?.length > 0) {
|
||||
const permissions = filteredCollections.map((collection) => convertToPermission(collection));
|
||||
|
||||
const orderedPermissions = [
|
||||
CollectionPermission.Manage,
|
||||
CollectionPermission.Edit,
|
||||
CollectionPermission.EditExceptPass,
|
||||
CollectionPermission.View,
|
||||
CollectionPermission.ViewExceptPass,
|
||||
];
|
||||
|
||||
return orderedPermissions.find((perm) => permissions.includes(perm));
|
||||
}
|
||||
|
||||
return "NoAccess";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/ser
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
@@ -1322,6 +1323,11 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DefaultServerSettingsService,
|
||||
useClass: DefaultServerSettingsService,
|
||||
deps: [ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RegisterRouteService,
|
||||
useClass: RegisterRouteService,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { RegisterRouteService } from "@bitwarden/auth/common";
|
||||
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, LinkModule, RouterModule],
|
||||
template: `
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-text-center" *ngIf="!(isUserRegistrationDisabled$ | async)">
|
||||
{{ "newToBitwarden" | i18n }}
|
||||
<a bitLink [routerLink]="registerRoute$ | async">{{ "createAccount" | i18n }}</a>
|
||||
</div>
|
||||
@@ -18,7 +19,10 @@ import { LinkModule } from "@bitwarden/components";
|
||||
})
|
||||
export class LoginSecondaryContentComponent {
|
||||
registerRouteService = inject(RegisterRouteService);
|
||||
serverSettingsService = inject(DefaultServerSettingsService);
|
||||
|
||||
// TODO: remove when email verification flag is removed
|
||||
protected registerRoute$ = this.registerRouteService.registerRoute$();
|
||||
|
||||
protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-->
|
||||
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<ng-container *ngIf="loginUiState === LoginUiState.EMAIL_ENTRY">
|
||||
<div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.EMAIL_ENTRY }">
|
||||
<!-- Email Address input -->
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
@@ -82,9 +82,9 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY">
|
||||
<div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY }">
|
||||
<!-- Master Password input -->
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
@@ -140,5 +140,5 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SemVer } from "semver";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
import { Region } from "../environment.service";
|
||||
|
||||
import { ServerConfig } from "./server-config";
|
||||
@@ -10,6 +11,8 @@ import { ServerConfig } from "./server-config";
|
||||
export abstract class ConfigService {
|
||||
/** The server config of the currently active user */
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
/** The server settings of the currently active user */
|
||||
serverSettings$: Observable<ServerSettings | null>;
|
||||
/** The cloud region of the currently active user */
|
||||
cloudRegion$: Observable<Region>;
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ThirdPartyServerConfigData,
|
||||
EnvironmentServerConfigData,
|
||||
} from "../../models/data/server-config.data";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
|
||||
const dayInMilliseconds = 24 * 3600 * 1000;
|
||||
|
||||
@@ -16,6 +17,7 @@ export class ServerConfig {
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: Date;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(serverConfigData: ServerConfigData) {
|
||||
this.version = serverConfigData.version;
|
||||
@@ -24,6 +26,7 @@ export class ServerConfig {
|
||||
this.utcDate = new Date(serverConfigData.utcDate);
|
||||
this.environment = serverConfigData.environment;
|
||||
this.featureStates = serverConfigData.featureStates;
|
||||
this.settings = serverConfigData.settings;
|
||||
|
||||
if (this.server?.name == null && this.server?.url == null) {
|
||||
this.server = null;
|
||||
|
||||
@@ -16,6 +16,9 @@ describe("ServerConfigData", () => {
|
||||
name: "test",
|
||||
url: "https://test.com",
|
||||
},
|
||||
settings: {
|
||||
disableUserRegistration: false,
|
||||
},
|
||||
environment: {
|
||||
cloudRegion: Region.EU,
|
||||
vault: "https://vault.com",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import { ServerSettings } from "../domain/server-settings";
|
||||
import {
|
||||
ServerConfigResponse,
|
||||
ThirdPartyServerConfigResponse,
|
||||
@@ -15,6 +16,7 @@ export class ServerConfigData {
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: string;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
|
||||
this.version = serverConfigResponse?.version;
|
||||
@@ -27,6 +29,7 @@ export class ServerConfigData {
|
||||
? new EnvironmentServerConfigData(serverConfigResponse.environment)
|
||||
: null;
|
||||
this.featureStates = serverConfigResponse?.featureStates;
|
||||
this.settings = new ServerSettings(serverConfigResponse.settings);
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ServerSettings } from "./server-settings";
|
||||
|
||||
describe("ServerSettings", () => {
|
||||
describe("disableUserRegistration", () => {
|
||||
it("defaults disableUserRegistration to false", () => {
|
||||
const settings = new ServerSettings();
|
||||
expect(settings.disableUserRegistration).toBe(false);
|
||||
});
|
||||
|
||||
it("sets disableUserRegistration to true when provided", () => {
|
||||
const settings = new ServerSettings({ disableUserRegistration: true });
|
||||
expect(settings.disableUserRegistration).toBe(true);
|
||||
});
|
||||
|
||||
it("sets disableUserRegistration to false when provided", () => {
|
||||
const settings = new ServerSettings({ disableUserRegistration: false });
|
||||
expect(settings.disableUserRegistration).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export class ServerSettings {
|
||||
disableUserRegistration: boolean;
|
||||
|
||||
constructor(data?: ServerSettings) {
|
||||
this.disableUserRegistration = data?.disableUserRegistration ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import { ServerSettings } from "../domain/server-settings";
|
||||
|
||||
export class ServerConfigResponse extends BaseResponse {
|
||||
version: string;
|
||||
@@ -8,6 +9,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
server: ThirdPartyServerConfigResponse;
|
||||
environment: EnvironmentServerConfigResponse;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -21,6 +23,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
|
||||
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
|
||||
this.featureStates = this.getResponseProperty("FeatureStates");
|
||||
this.settings = new ServerSettings(this.getResponseProperty("Settings"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Environment, EnvironmentService, Region } from "../../abstractions/envi
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { devFlagEnabled, devFlagValue } from "../../misc/flags";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
|
||||
@@ -57,6 +58,8 @@ export class DefaultConfigService implements ConfigService {
|
||||
|
||||
serverConfig$: Observable<ServerConfig>;
|
||||
|
||||
serverSettings$: Observable<ServerSettings>;
|
||||
|
||||
cloudRegion$: Observable<Region>;
|
||||
|
||||
constructor(
|
||||
@@ -111,6 +114,10 @@ export class DefaultConfigService implements ConfigService {
|
||||
this.cloudRegion$ = this.serverConfig$.pipe(
|
||||
map((config) => config?.environment?.cloudRegion ?? Region.US),
|
||||
);
|
||||
|
||||
this.serverSettings$ = this.serverConfig$.pipe(
|
||||
map((config) => config?.settings ?? new ServerSettings()),
|
||||
);
|
||||
}
|
||||
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "../abstractions/config/config.service";
|
||||
import { ServerSettings } from "../models/domain/server-settings";
|
||||
|
||||
import { DefaultServerSettingsService } from "./default-server-settings.service";
|
||||
|
||||
describe("DefaultServerSettingsService", () => {
|
||||
let service: DefaultServerSettingsService;
|
||||
let configServiceMock: { serverSettings$: any };
|
||||
|
||||
beforeEach(() => {
|
||||
configServiceMock = { serverSettings$: of() };
|
||||
service = new DefaultServerSettingsService(configServiceMock as ConfigService);
|
||||
});
|
||||
|
||||
describe("getSettings$", () => {
|
||||
it("returns server settings", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: true });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.getSettings$().subscribe((settings) => {
|
||||
expect(settings).toEqual(mockSettings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isUserRegistrationDisabled$", () => {
|
||||
it("returns true when user registration is disabled", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: true });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
|
||||
expect(isDisabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when user registration is enabled", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: false });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
|
||||
expect(isDisabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { ConfigService } from "../abstractions/config/config.service";
|
||||
import { ServerSettings } from "../models/domain/server-settings";
|
||||
|
||||
export class DefaultServerSettingsService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSettings$(): Observable<ServerSettings> {
|
||||
return this.configService.serverSettings$;
|
||||
}
|
||||
|
||||
get isUserRegistrationDisabled$(): Observable<boolean> {
|
||||
return this.getSettings$().pipe(
|
||||
map((settings: ServerSettings) => settings.disableUserRegistration),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, HostBinding, Input, OnInit } from "@angular/core";
|
||||
|
||||
import type { SortFn } from "./table-data-source";
|
||||
import type { SortDirection, SortFn } from "./table-data-source";
|
||||
import { TableComponent } from "./table.component";
|
||||
|
||||
@Component({
|
||||
@@ -19,12 +19,16 @@ export class SortableComponent implements OnInit {
|
||||
*/
|
||||
@Input() bitSortable: string;
|
||||
|
||||
private _default: boolean;
|
||||
private _default: SortDirection | boolean = false;
|
||||
/**
|
||||
* Mark the column as the default sort column
|
||||
*/
|
||||
@Input() set default(value: boolean | "") {
|
||||
this._default = coerceBooleanProperty(value);
|
||||
@Input() set default(value: SortDirection | boolean | "") {
|
||||
if (value === "desc" || value === "asc") {
|
||||
this._default = value;
|
||||
} else {
|
||||
this._default = coerceBooleanProperty(value) ? "asc" : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +36,11 @@ export class SortableComponent implements OnInit {
|
||||
*
|
||||
* @example
|
||||
* fn = (a, b) => a.name.localeCompare(b.name)
|
||||
*
|
||||
* fn = (a, b, direction) => {
|
||||
* const result = a.name.localeCompare(b.name)
|
||||
* return direction === 'asc' ? result : -result;
|
||||
* }
|
||||
*/
|
||||
@Input() fn: SortFn;
|
||||
|
||||
@@ -52,8 +61,18 @@ export class SortableComponent implements OnInit {
|
||||
|
||||
protected setActive() {
|
||||
if (this.table.dataSource) {
|
||||
const direction = this.isActive && this.direction === "asc" ? "desc" : "asc";
|
||||
this.table.dataSource.sort = { column: this.bitSortable, direction: direction, fn: this.fn };
|
||||
const defaultDirection = this._default === "desc" ? "desc" : "asc";
|
||||
const direction = this.isActive
|
||||
? this.direction === "asc"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: defaultDirection;
|
||||
|
||||
this.table.dataSource.sort = {
|
||||
column: this.bitSortable,
|
||||
direction: direction,
|
||||
fn: this.fn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DataSource } from "@angular/cdk/collections";
|
||||
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
|
||||
|
||||
export type SortDirection = "asc" | "desc";
|
||||
export type SortFn = (a: any, b: any) => number;
|
||||
export type SortFn = (a: any, b: any, direction?: SortDirection) => number;
|
||||
export type Sort = {
|
||||
column?: string;
|
||||
direction: SortDirection;
|
||||
@@ -166,7 +166,7 @@ export class TableDataSource<T> extends DataSource<T> {
|
||||
return data.sort((a, b) => {
|
||||
// If a custom sort function is provided, use it instead of the default.
|
||||
if (sort.fn) {
|
||||
return sort.fn(a, b) * directionModifier;
|
||||
return sort.fn(a, b, sort.direction) * directionModifier;
|
||||
}
|
||||
|
||||
let valueA = this.sortingDataAccessor(a, column);
|
||||
|
||||
@@ -105,7 +105,7 @@ within the `ng-template`which provides access to the rows using `let-rows$`.
|
||||
|
||||
We provide a simple component for displaying sortable column headers. The `bitSortable` component
|
||||
wires up to the `TableDataSource` and will automatically sort the data when clicked and display an
|
||||
indicator for which column is currently sorted. The dafault sorting can be specified by setting the
|
||||
indicator for which column is currently sorted. The default sorting can be specified by setting the
|
||||
`default`.
|
||||
|
||||
```html
|
||||
@@ -113,10 +113,23 @@ indicator for which column is currently sorted. The dafault sorting can be speci
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
```
|
||||
|
||||
For default sorting in descending order, set default="desc"
|
||||
|
||||
```html
|
||||
<th bitCell bitSortable="name" default="desc">Name</th>
|
||||
```
|
||||
|
||||
It's also possible to define a custom sorting function by setting the `fn` input.
|
||||
|
||||
```ts
|
||||
// Basic sort function
|
||||
const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1);
|
||||
|
||||
// Direction aware sort function
|
||||
const sortByName = (a: T, b: T, direction?: SortDirection) => {
|
||||
const result = a.name.localeCompare(b.name);
|
||||
return direction === "asc" ? result : -result;
|
||||
};
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
@@ -12,11 +12,6 @@ import {
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Splits an email into a username, subaddress, and domain named group.
|
||||
* Subaddress is optional.
|
||||
*/
|
||||
export const DOMAIN_PARSER = new RegExp("[^@]+@(?<domain>.+)");
|
||||
|
||||
/** Options group for catchall emails */
|
||||
@Component({
|
||||
selector: "tools-catchall-settings",
|
||||
|
||||
@@ -39,14 +39,12 @@
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="userId$ | async"
|
||||
@@ -84,25 +82,21 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||
[userId]="this.userId$ | async"
|
||||
@@ -45,7 +44,6 @@
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="this.userId$ | async"
|
||||
|
||||
@@ -59,25 +59,21 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
Reference in New Issue
Block a user