diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index d6bcd76903b..653d05ef129 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -16,13 +16,39 @@ "all" | i18n }} - {{ "name" | i18n }} + + + {{ "name" | i18n }} + + + + {{ "name" | i18n }} + {{ "owner" | i18n }} {{ "collections" | i18n }} - {{ "groups" | i18n }} - + + {{ "groups" | i18n }} + + {{ "permission" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index ec91b135350..1c658663196 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -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 { 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"; + } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 5bf81761ed6..340e8f567cb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -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, diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index abc772b6c14..dbc9535e67a 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -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: ` -
+
{{ "newToBitwarden" | i18n }} {{ "createAccount" | i18n }}
@@ -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$; } diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index 888b2e86390..0afc4659db3 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -11,7 +11,7 @@ -->
- +
{{ "emailAddress" | i18n }} @@ -82,9 +82,9 @@
-
+
- +
{{ "masterPass" | i18n }} @@ -140,5 +140,5 @@
-
+ diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts index 9b16cee3854..05a3dcd148c 100644 --- a/libs/common/src/platform/abstractions/config/config.service.ts +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -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; + /** The server settings of the currently active user */ + serverSettings$: Observable; /** The cloud region of the currently active user */ cloudRegion$: Observable; /** diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index bb186059641..b51628cbf5b 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -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; diff --git a/libs/common/src/platform/models/data/server-config.data.spec.ts b/libs/common/src/platform/models/data/server-config.data.spec.ts index b94092662a6..13d14204085 100644 --- a/libs/common/src/platform/models/data/server-config.data.spec.ts +++ b/libs/common/src/platform/models/data/server-config.data.spec.ts @@ -16,6 +16,9 @@ describe("ServerConfigData", () => { name: "test", url: "https://test.com", }, + settings: { + disableUserRegistration: false, + }, environment: { cloudRegion: Region.EU, vault: "https://vault.com", diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index 57e8fbc6284..d5f17fd0ace 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -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) { 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 { diff --git a/libs/common/src/platform/models/domain/server-settings.spec.ts b/libs/common/src/platform/models/domain/server-settings.spec.ts new file mode 100644 index 00000000000..3e6295fa5c4 --- /dev/null +++ b/libs/common/src/platform/models/domain/server-settings.spec.ts @@ -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); + }); + }); +}); diff --git a/libs/common/src/platform/models/domain/server-settings.ts b/libs/common/src/platform/models/domain/server-settings.ts new file mode 100644 index 00000000000..b18f07466d7 --- /dev/null +++ b/libs/common/src/platform/models/domain/server-settings.ts @@ -0,0 +1,7 @@ +export class ServerSettings { + disableUserRegistration: boolean; + + constructor(data?: ServerSettings) { + this.disableUserRegistration = data?.disableUserRegistration ?? false; + } +} diff --git a/libs/common/src/platform/models/response/server-config.response.ts b/libs/common/src/platform/models/response/server-config.response.ts index a546d2d3de7..d295634830a 100644 --- a/libs/common/src/platform/models/response/server-config.response.ts +++ b/libs/common/src/platform/models/response/server-config.response.ts @@ -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")); } } diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e0603ed509b..fce1c12106f 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -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; + serverSettings$: Observable; + cloudRegion$: Observable; 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$(key: Flag) { diff --git a/libs/common/src/platform/services/default-server-settings.service.spec.ts b/libs/common/src/platform/services/default-server-settings.service.spec.ts new file mode 100644 index 00000000000..09bca2ff786 --- /dev/null +++ b/libs/common/src/platform/services/default-server-settings.service.spec.ts @@ -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); + }); + }); + }); +}); diff --git a/libs/common/src/platform/services/default-server-settings.service.ts b/libs/common/src/platform/services/default-server-settings.service.ts new file mode 100644 index 00000000000..9d0dd4bfd94 --- /dev/null +++ b/libs/common/src/platform/services/default-server-settings.service.ts @@ -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 { + return this.configService.serverSettings$; + } + + get isUserRegistrationDisabled$(): Observable { + return this.getSettings$().pipe( + map((settings: ServerSettings) => settings.disableUserRegistration), + ); + } +} diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index b2e456953b1..c6d60f155b2 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -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, + }; } } diff --git a/libs/components/src/table/table-data-source.ts b/libs/components/src/table/table-data-source.ts index 6501c9bffbd..8a5d994dc18 100644 --- a/libs/components/src/table/table-data-source.ts +++ b/libs/components/src/table/table-data-source.ts @@ -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 extends DataSource { 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); diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index 3f28dd93b68..8d784190ed9 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -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 Name ``` +For default sorting in descending order, set default="desc" + +```html +Name +``` + 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 diff --git a/libs/tools/generator/components/src/catchall-settings.component.ts b/libs/tools/generator/components/src/catchall-settings.component.ts index 74fb37d2335..3a42d682971 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.ts +++ b/libs/tools/generator/components/src/catchall-settings.component.ts @@ -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("[^@]+@(?.+)"); - /** Options group for catchall emails */ @Component({ selector: "tools-catchall-settings", diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 0182bd1c204..ce86abe80ae 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -39,14 +39,12 @@