mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +00:00
Merge EC-73 Again After Rebase (#4104)
* [EC-73] feat: add inital version of modal using dialog service * [EC-73] feat: create story for dialog * [EC-73] feat: setup story with support for injected data * [EC-73] feat: add inital version of subtitle * [EC-73] feat: add tabs * [EC-73] feat: initial version of collection info form * [EC-73] feat: start of working form * [EC-73] feat: add custom form validator * [EC-73] fix: dialog directive names after rebase * [EC-73] feat: use custom validator * [EC-73] fix: story * [EC-73] feat: allow parent picking * [EC-73] feat: remove tabs to allow for merging * [EC-73] feat: extend story with new and edit dialogs * [EC-73] feat: change title depending on if editing or not * [EC-73] fix: parent not connected to form * [EC-73] feat: add organizationId to dialog data * [EC-73] feat: only allow nesting within collections with access * [EC-73] feat: handle loading with spinner * [EC-73] feat: update collections on submit * [EC-73] feat: reload on save * [EC-73] feat: update story to work with latest changes * [EC-73] feat: always fetch collections from server * [EC-73] fix: do not submit if form invalid * [EC-73] feat: create new collections using new ui * [EC-73] fix: external id not being saved * [EC-73] chore: move calls to separete collection admin service * [EC-73] feat: use new admin views * [EC-73] feat: implement deletion * [EC-73] feat: add support for collection details in service * [EC-73] fix: story * [EC-73] fix: cancel button * [EC-73] feat: re-add tabs * [EC-73] fix: jslib service collection deps * [EC-73] chore: rename component to collection-dialog * [EC-73] chore: clean up collection api service which was replaced * [EC-73] chore: restore collection.service * [EC-73] chore: restore dialog component changes * [EC-73] fix: move subscription to ngOnInit * [EC-73] feat: disable padding when using tabbed content * [EC-73] chore: re-add collections page * [EC-73] chore: move component to shared org module * [EC-73] feat: add empty access selector * [EC-73] feat: add groups to access selector * [EC-73] chore: improve storybook support * [EC-73] feat: tweak item assignment * [EC-73] feat: add support for showing users * [EC-73] feat: use async actions * [EC-73] chore: clean up casting * [EC-73] fix: permissions not loading correctly in access selector * [EC-73] feat: implement saving group permissions * [EC-73] feat: rename to collection access selection view * [EC-73] feat: save users as well * [EC-73] fix: access selector usage * [EC-73] feat: new collection creation * [EC-73] feat: fetch users from collection details * [EC-73] chore: clean up * [EC-73] fix: circular dependency issues * [EC-73] fix: import shared module directly to workaround build issues * [EC-73] fix: missing dependencies in story * [EC-73] chore: move story * [EC-73] feat: hide delete button if no permission * [EC-73] feat: properly handle orgs without groups * [EC-73] fix: use correct functions in template * [EC-73] feat: properly handle non-existing parent * [EC-73] chore: use double ngIf instead of else template * [EC-73] fix: add type to dialog ref * [EC-73] fix: restrict field modifiers * [EC-73] fix: use result enum directly * [EC-73] fix: simplify mapping logic * [EC-73] * [EC-73] feat: add story for free orgs without groups * [EC-73] fix: parametrized i18n * [EC-73] feat: create new shared org module * [EC-73] feat: move collection dialog to shared * [EC-73] feat: move access selector to shared * [EC-73] feat: create core organization module * [EC-73] feat: move collection admin service to web * [EC-73] feat: move collection admin views to web * [EC-73] fix: missing i18n * [EC-73] fix: refactor for type safety * [EC-73] fix: storybook not compiling again * [EC-73] feat: use helper function to open dialog * [EC-73] chore: remove comment * [EC-73] fix: only show delete if in edit mode * [EC-73] chore: remove ngIf else in template * [EC-73] fix: add missing appA11yTitle * [EC-73] chore: rename remove to delete * [EC-73] chore: refactor ngOnInit * [EC-73] fix: dialog position strategy * [EC-73] fix: revert spinner to old way of doing it * Fix remaining errors after rebase/merge * fix: import shared module directly Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
This commit is contained in:
@@ -92,7 +92,7 @@ import { BillingPaymentResponse } from "../models/response/billing-payment.respo
|
||||
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
||||
import { CipherResponse } from "../models/response/cipher.response";
|
||||
import {
|
||||
CollectionGroupDetailsResponse,
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../models/response/collection.response";
|
||||
import { DeviceVerificationResponse } from "../models/response/device-verification.response";
|
||||
@@ -314,7 +314,7 @@ export abstract class ApiService {
|
||||
getCollectionDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<CollectionGroupDetailsResponse>;
|
||||
) => Promise<CollectionAccessDetailsResponse>;
|
||||
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
|
||||
|
||||
@@ -440,6 +440,10 @@ export class Utils {
|
||||
return mobile || win.navigator.userAgent.match(/iPad/i) != null;
|
||||
}
|
||||
|
||||
static delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private static isAppleMobile(win: Window) {
|
||||
return (
|
||||
win.navigator.userAgent.match(/iPhone/i) != null ||
|
||||
|
||||
@@ -6,6 +6,7 @@ export class CollectionRequest {
|
||||
name: string;
|
||||
externalId: string;
|
||||
groups: SelectionReadOnlyRequest[] = [];
|
||||
users: SelectionReadOnlyRequest[] = [];
|
||||
|
||||
constructor(collection?: Collection) {
|
||||
if (collection == null) {
|
||||
|
||||
@@ -25,8 +25,9 @@ export class CollectionDetailsResponse extends CollectionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionGroupDetailsResponse extends CollectionResponse {
|
||||
export class CollectionAccessDetailsResponse extends CollectionResponse {
|
||||
groups: SelectionReadOnlyResponse[] = [];
|
||||
users: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -34,5 +35,10 @@ export class CollectionGroupDetailsResponse extends CollectionResponse {
|
||||
if (groups != null) {
|
||||
this.groups = groups.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
|
||||
const users = this.getResponseProperty("Users");
|
||||
if (users != null) {
|
||||
this.users = users.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Collection } from "../domain/collection";
|
||||
import { ITreeNodeObject } from "../domain/tree-node";
|
||||
import { CollectionGroupDetailsResponse } from "../response/collection.response";
|
||||
import { CollectionAccessDetailsResponse } from "../response/collection.response";
|
||||
|
||||
import { View } from "./view";
|
||||
|
||||
@@ -12,7 +12,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
readOnly: boolean = null;
|
||||
hidePasswords: boolean = null;
|
||||
|
||||
constructor(c?: Collection | CollectionGroupDetailsResponse) {
|
||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import { BillingPaymentResponse } from "../models/response/billing-payment.respo
|
||||
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
||||
import { CipherResponse } from "../models/response/cipher.response";
|
||||
import {
|
||||
CollectionGroupDetailsResponse,
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../models/response/collection.response";
|
||||
import { DeviceVerificationResponse } from "../models/response/device-verification.response";
|
||||
@@ -810,7 +810,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
async getCollectionDetails(
|
||||
organizationId: string,
|
||||
id: string
|
||||
): Promise<CollectionGroupDetailsResponse> {
|
||||
): Promise<CollectionAccessDetailsResponse> {
|
||||
const r = await this.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/collections/" + id + "/details",
|
||||
@@ -818,7 +818,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new CollectionGroupDetailsResponse(r);
|
||||
return new CollectionAccessDetailsResponse(r);
|
||||
}
|
||||
|
||||
async getUserCollections(): Promise<ListResponse<CollectionResponse>> {
|
||||
|
||||
@@ -91,10 +91,6 @@ export class CollectionService implements CollectionServiceAbstraction {
|
||||
return decryptedCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated August 30 2022: Moved to new Vault Filter Service
|
||||
* Remove when Desktop and Browser are updated
|
||||
*/
|
||||
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
|
||||
if (collections == null) {
|
||||
collections = await this.getAllDecrypted();
|
||||
|
||||
@@ -3,10 +3,9 @@ import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
|
||||
import { BitActionDirective } from "./bit-action.directive";
|
||||
import { BitSubmitDirective } from "./bit-submit.directive";
|
||||
|
||||
import { BitActionDirective } from ".";
|
||||
|
||||
/**
|
||||
* This directive has two purposes:
|
||||
*
|
||||
|
||||
@@ -4,7 +4,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { CalloutComponent } from ".";
|
||||
import { CalloutComponent } from "./callout.component";
|
||||
|
||||
describe("Callout", () => {
|
||||
let component: CalloutComponent;
|
||||
|
||||
56
libs/components/src/form-field/bit-validators.stories.ts
Normal file
56
libs/components/src/form-field/bit-validators.stories.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { forbiddenCharacters } from "./bit-validators/forbidden-characters.validator";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Form/Custom Validators",
|
||||
component: BitFormFieldComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
inputForbiddenCharacters: (chars) =>
|
||||
`The following characters are not allowed: ${chars}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const template = `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
</form>`;
|
||||
|
||||
export const ForbiddenCharacters: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: new FormBuilder().group({
|
||||
name: ["", forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"])],
|
||||
}),
|
||||
},
|
||||
template,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FormControl } from "@angular/forms";
|
||||
|
||||
import { forbiddenCharacters } from "./forbidden-characters.validator";
|
||||
|
||||
describe("forbiddenCharacters", () => {
|
||||
it("should return no error when input is null", () => {
|
||||
const input = createControl(null);
|
||||
const validate = forbiddenCharacters(["n", "u", "l", "l"]);
|
||||
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should return no error when no characters are forbidden", () => {
|
||||
const input = createControl("special characters: \\/@#$%^&*()");
|
||||
const validate = forbiddenCharacters([]);
|
||||
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should return no error when input does not contain forbidden characters", () => {
|
||||
const input = createControl("contains no special characters");
|
||||
const validate = forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"]);
|
||||
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should return error when input contains forbidden characters", () => {
|
||||
const input = createControl("contains / illegal @ characters");
|
||||
const validate = forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"]);
|
||||
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
function createControl(input: string) {
|
||||
return new FormControl(input);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
export function forbiddenCharacters(characters: string[]): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (!(control instanceof FormControl)) {
|
||||
throw new Error("forbiddenCharacters only supports validating FormControls");
|
||||
}
|
||||
|
||||
if (control.value === null || control.value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = String(control.value);
|
||||
|
||||
for (const char of value) {
|
||||
if (characters.includes(char)) {
|
||||
return { forbiddenCharacters: { value: control.value, characters } };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
1
libs/components/src/form-field/bit-validators/index.ts
Normal file
1
libs/components/src/form-field/bit-validators/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { forbiddenCharacters } from "./forbidden-characters.validator";
|
||||
@@ -30,6 +30,8 @@ export class BitErrorComponent {
|
||||
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
|
||||
case "maxlength":
|
||||
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
|
||||
case "forbiddenCharacters":
|
||||
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
||||
default:
|
||||
// Attempt to show a custom error message.
|
||||
if (this.error[1]?.message) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./form-field.module";
|
||||
export * from "./form-field.component";
|
||||
export * from "./form-field-control";
|
||||
export * as BitValidators from "./bit-validators";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./multi-select.module";
|
||||
export * from "./models/select-item-view";
|
||||
|
||||
Reference in New Issue
Block a user