diff --git a/.storybook/main.ts b/.storybook/main.ts index cb63ada550b3..26eee201f990 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm"; const config: StorybookConfig = { stories: [ + "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 69b9b78819f7..765637be396e 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -23,8 +23,8 @@ export class ReportsHomeComponent implements OnInit { ngOnInit() { this.homepage$ = this.router.events.pipe( filter((event) => event instanceof NavigationEnd), - map((event) => (event as NavigationEnd).urlAfterRedirects.endsWith("/reports")), - startWith(true), + map((event) => this.isReportsHomepageRouteUrl((event as NavigationEnd).urlAfterRedirects)), + startWith(this.isReportsHomepageRouteUrl(this.router.url)), ); this.reports$ = this.route.params.pipe( @@ -61,4 +61,8 @@ export class ReportsHomeComponent implements OnInit { }, ]; } + + private isReportsHomepageRouteUrl(url: string): boolean { + return url.endsWith("/reports"); + } } diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.html b/apps/web/src/app/auth/anon-layout-wrapper.component.html new file mode 100644 index 000000000000..26cab3080957 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.ts b/apps/web/src/app/auth/anon-layout-wrapper.component.ts new file mode 100644 index 000000000000..e39a8e11a993 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.ts @@ -0,0 +1,34 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, RouterModule } from "@angular/router"; + +import { AnonLayoutComponent } from "@bitwarden/auth/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Icon } from "@bitwarden/components"; + +@Component({ + standalone: true, + templateUrl: "anon-layout-wrapper.component.html", + imports: [AnonLayoutComponent, RouterModule], +}) +export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { + protected pageTitle: string; + protected pageSubtitle: string; + protected pageIcon: Icon; + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + ) { + this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]); + this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]); + this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate + } + + ngOnInit() { + document.body.classList.add("layout_frontend"); + } + + ngOnDestroy() { + document.body.classList.remove("layout_frontend"); + } +} diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index d03b6dcc3851..897d360b4be8 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -20,7 +20,7 @@ bitLink [disabled]="disabled" type="button" - class="tw-w-full tw-truncate tw-text-start tw-leading-snug" + class="tw-flex tw-w-full tw-text-start tw-leading-snug" linkType="secondary" title="{{ 'viewCollectionWithName' | i18n: collection.name }}" [routerLink]="[]" @@ -28,7 +28,15 @@ queryParamsHandling="merge" appStopProp > - {{ collection.name }} + {{ collection.name }} +
+ {{ "addAccess" | i18n }} +
diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 8bf7779f8864..4a9667f8b8f3 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -21,6 +21,7 @@ import { RowHeightClass } from "./vault-items.component"; }) export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; + protected Unassigned = "unassigned"; @Input() disabled: boolean; @Input() collection: CollectionView; 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 c63273fabd3b..ba69c038fb38 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 @@ -99,8 +99,12 @@ (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > + o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user without edit access should not see the Edit option unless that user has "Can Manage" access to a collection + if ( + !collection.manage && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.editAnyCollection + ) { + return false; + } + //Owner/Admin and Custom Users with Edit can see Edit and Access of Orphaned Collections + if ( + collection.addAccess && + collection.id !== Unassigned && + ((organization?.type === OrganizationUserType.Custom && + organization?.permissions.editAnyCollection) || + organization.isAdmin || + organization.isOwner) + ) { + return true; + } + } return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); } @@ -111,6 +136,32 @@ export class VaultItemsComponent { } const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user with only edit access should not see the Delete button for orphaned collections + if ( + collection.addAccess && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.deleteAnyCollection && + organization?.permissions.editAnyCollection + ) { + return false; + } + + // Owner/Admin with no access to a collection will not see Delete + if ( + !collection.assigned && + !collection.addAccess && + (organization.isAdmin || organization.isOwner) && + !( + organization?.type === OrganizationUserType.Custom && + organization?.permissions.deleteAnyCollection + ) + ) { + return false; + } + } + return collection.canDelete(organization); } diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 2be84b0d2468..cc217fc9ceff 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -1,3 +1,4 @@ +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -7,6 +8,7 @@ import { CollectionAccessSelectionView } from "../../../admin-console/organizati export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; users: CollectionAccessSelectionView[] = []; + addAccess: boolean; /** * Flag indicating the user has been explicitly assigned to this Collection @@ -31,6 +33,33 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } + groupsCanManage() { + if (this.groups.length === 0) { + return this.groups; + } + + const returnedGroups = this.groups.filter((group) => { + if (group.manage) { + return group; + } + }); + return returnedGroups; + } + + usersCanManage(revokedUsers: OrganizationUserUserDetailsResponse[]) { + if (this.users.length === 0) { + return this.users; + } + + const returnedUsers = this.users.filter((user) => { + const isRevoked = revokedUsers.some((revoked) => revoked.id === user.id); + if (user.manage && !isRevoked) { + return user; + } + }); + return returnedUsers; + } + /** * Whether the current user can edit the collection, including user and group access */ diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index f815fccb213d..af7b5059e524 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -26,6 +26,20 @@
+ + + {{ "all" | i18n }} + + + + {{ "addAccess" | i18n }} + + {{ trashCleanupWarning }} @@ -54,6 +68,8 @@ [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" + [addAccessStatus]="addAccessStatus$ | async" + [addAccessToggle]="showAddAccessToggle" > diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 6cecf938b9ea..f037170dda51 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -36,6 +36,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -102,6 +105,11 @@ import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; +enum AddAccessStatusType { + All = 0, + AddAccess = 1, +} + @Component({ selector: "app-org-vault", templateUrl: "vault.component.html", @@ -122,6 +130,7 @@ export class VaultComponent implements OnInit, OnDestroy { trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); + protected showAddAccessToggle = false; protected noItemIcon = Icons.Search; protected performingInitialLoad = true; protected refreshing = false; @@ -149,10 +158,12 @@ export class VaultComponent implements OnInit, OnDestroy { protected get flexibleCollectionsV1Enabled(): boolean { return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; } + protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); + protected addAccessStatus$ = new BehaviorSubject(0); constructor( private route: ActivatedRoute, @@ -181,6 +192,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, + private organizationUserService: OrganizationUserService, protected configService: ConfigService, ) {} @@ -241,6 +253,11 @@ export class VaultComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe((activeFilter) => { this.activeFilter = activeFilter; + + // watch the active filters. Only show toggle when viewing the collections filter + if (!this.activeFilter.collectionId) { + this.showAddAccessToggle = false; + } }); this.searchText$ @@ -309,6 +326,10 @@ export class VaultComponent implements OnInit, OnDestroy { const allCiphers$ = organization$.pipe( concatMap(async (organization) => { + // If user swaps organization reset the addAccessToggle + if (!this.showAddAccessToggle || organization) { + this.addAccessToggle(0); + } let ciphers; if (this.flexibleCollectionsV1Enabled) { @@ -348,9 +369,21 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( + // This will be passed into the usersCanManage call + this.orgRevokedUsers = ( + await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$)) + ).data.filter((user: OrganizationUserUserDetailsResponse) => { + return user.status === -1; + }); + + const collections$ = combineLatest([ + nestedCollections$, + filter$, + this.currentSearchText$, + this.addAccessStatus$, + ]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText, addAccessStatus]) => { if ( filter.collectionId === Unassigned || (filter.collectionId === undefined && filter.type !== undefined) @@ -358,26 +391,30 @@ export class VaultComponent implements OnInit, OnDestroy { return []; } + this.showAddAccessToggle = false; let collectionsToReturn = []; if (filter.collectionId === undefined || filter.collectionId === All) { - collectionsToReturn = collections.map((c) => c.node); + collectionsToReturn = await this.addAccessCollectionsMap(collections); } else { const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( collections, filter.collectionId, ); - collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + collectionsToReturn = await this.addAccessCollectionsMap(selectedCollection?.children); } if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, - (collection) => collection.name, - (collection) => collection.id, + (collection: CollectionAdminView) => collection.name, + (collection: CollectionAdminView) => collection.id, ); } + if (addAccessStatus === 1 && this.showAddAccessToggle) { + collectionsToReturn = collectionsToReturn.filter((c: any) => c.addAccess); + } return collectionsToReturn; }), takeUntil(this.destroy$), @@ -586,6 +623,57 @@ export class VaultComponent implements OnInit, OnDestroy { ); } + // Update the list of collections to see if any collection is orphaned + // and will receive the addAccess badge / be filterable by the user + async addAccessCollectionsMap(collections: TreeNode[]) { + let mappedCollections; + const { type, allowAdminAccessToAllCollectionItems, permissions } = this.organization; + + const canEditCiphersCheck = + this._flexibleCollectionsV1FlagEnabled && + !this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled); + + // This custom type check will show addAccess badge for + // Custom users with canEdit access AND owner/admin manage access setting is OFF + const customUserCheck = + this._flexibleCollectionsV1FlagEnabled && + !allowAdminAccessToAllCollectionItems && + type === OrganizationUserType.Custom && + permissions.editAnyCollection; + + // If Custom user has Delete Only access they will not see Add Access toggle + const customUserOnlyDelete = + this.flexibleCollectionsV1Enabled && + type === OrganizationUserType.Custom && + permissions.deleteAnyCollection && + !permissions.editAnyCollection; + + if (!customUserOnlyDelete && (canEditCiphersCheck || customUserCheck)) { + mappedCollections = collections.map((c: TreeNode) => { + const groupsCanManage = c.node.groupsCanManage(); + const usersCanManage = c.node.usersCanManage(this.orgRevokedUsers); + if ( + groupsCanManage.length === 0 && + usersCanManage.length === 0 && + c.node.id !== Unassigned + ) { + c.node.addAccess = true; + this.showAddAccessToggle = true; + } else { + c.node.addAccess = false; + } + return c.node; + }); + } else { + mappedCollections = collections.map((c: TreeNode) => c.node); + } + return mappedCollections; + } + + addAccessToggle(e: any) { + this.addAccessStatus$.next(e); + } + get loading() { return this.refreshing || this.processingEvent; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4840003abdfe..f032e822f86e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index a944f9dd6729..e80bf6a834de 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base"); config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/auth/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", ]; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 1f583edf20b6..55da36d9bfac 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,5 +1,5 @@
@@ -13,8 +13,10 @@

{{ subtitle }}

-
-
+
+
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index d247a010bfc0..106844fb5aa2 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -5,7 +5,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { IconModule, Icon } from "../../../../components/src/icon"; import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogo } from "../../icons/bitwarden-logo"; +import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; @Component({ standalone: true, diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/auth/src/angular/anon-layout/anon-layout.mdx new file mode 100644 index 000000000000..c604c02f0304 --- /dev/null +++ b/libs/auth/src/angular/anon-layout/anon-layout.mdx @@ -0,0 +1,118 @@ +import { Meta, Story, Controls } from "@storybook/addon-docs"; + +import * as stories from "./anon-layout.stories"; + + + +# AnonLayout Component + +The Auth-owned AnonLayoutComponent is to be used for unauthenticated pages, where we don't know who +the user is (this includes viewing a Send). + +--- + +### Incorrect Usage ❌ + +The AnonLayoutComponent is **not** to be implemented by every component that uses it in that +component's template directly. For example, if you have a component template called +`example.component.html`, and you want it to use the AnonLayoutComponent, you will **not** be +writing: + +```html + + + +
Example component content
+
+``` + +### Correct Usage ✅ + +Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which +gives us the advantages of nested routes in Angular. + +To allow for routable composition, Auth will also provide a wrapper component in each client, called +AnonLayout**Wrapper**Component. + +For clarity: + +- AnonLayoutComponent = the Auth-owned library component - `` +- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client + routing module + +The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets: + +```html + + + + + + +``` + +To implement, the developer does not need to work with the base AnonLayoutComponent directly. The +devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for +example) to construct the page via routable composition: + +```javascript +// File: oss-routing.module.ts + +{ + path: "", + component: AnonLayoutWrapperComponent, // Wrapper component + children: [ + { + path: "sample-route", // replace with your route + children: [ + { + path: "", + component: MyPrimaryComponent, // replace with your component + }, + { + path: "", + component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) + outlet: "secondary", + }, + ], + data: { + pageTitle: "logIn", // example of a translation key from messages.json + pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json + pageIcon: LockIcon, // example of an icon to pass in + }, + }, + ], + }, +``` + +And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, +then your work will be as simple as just adding another child route under the `children` array. + +### Data Properties + +In the `oss-routing.module.ts` example above, notice the data properties being passed in: + +- For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. +- For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon + directly. + +All 3 of these properties are optional. + +```javascript +import { LockIcon } from "@bitwarden/auth/angular"; + +// ... + +{ + // ... + data: { + pageTitle: "logIn", + pageSubtitle: "loginWithMasterPassword", + pageIcon: LockIcon, + }, +} +``` + +--- + + diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index daba5b5e53c1..61a395b1559d 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -3,12 +3,12 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../../../../components/src/button"; -import { IconLock } from "../../icons/icon-lock"; +import { LockIcon } from "../icons"; import { AnonLayoutComponent } from "./anon-layout.component"; class MockPlatformUtilsService implements Partial { - getApplicationVersion = () => Promise.resolve("Version 2023.1.1"); + getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); } export default { @@ -28,7 +28,7 @@ export default { args: { title: "The Page Title", subtitle: "The subtitle (optional)", - icon: IconLock, + icon: LockIcon, }, } as Meta; @@ -38,14 +38,13 @@ export const WithPrimaryContent: Story = { render: (args) => ({ props: args, template: - /** - * The projected content (i.e. the
) and styling below is just a - * sample and could be replaced with any content and styling - */ + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` -
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
+
Primary Projected Content Area (customizable)
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
`, }), @@ -55,15 +54,16 @@ export const WithSecondaryContent: Story = { render: (args) => ({ props: args, template: - // Notice that slot="secondary" is requred to project any secondary content: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + // Notice that slot="secondary" is requred to project any secondary content. ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
+
Secondary Projected Content (optional)
@@ -75,14 +75,16 @@ export const WithSecondaryContent: Story = { export const WithLongContent: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
+
Secondary Projected Content (optional)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

@@ -95,9 +97,11 @@ export const WithLongContent: Story = { export const WithIcon: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts similarity index 100% rename from libs/auth/src/icons/bitwarden-logo.ts rename to libs/auth/src/angular/icons/bitwarden-logo.icon.ts diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 7bb3f575796b..d71e2e6efde9 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1 +1,3 @@ +export * from "./bitwarden-logo.icon"; +export * from "./lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/angular/icons/lock.icon.ts similarity index 98% rename from libs/auth/src/icons/icon-lock.ts rename to libs/auth/src/angular/icons/lock.icon.ts index 61330fe0df5a..b567c213f706 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/angular/icons/lock.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const IconLock = svgIcon` +export const LockIcon = svgIcon` diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index c93bf1c1d3ed..067ed63b8ef1 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -5,6 +5,7 @@ // icons export * from "./icons"; +export * from "./anon-layout/anon-layout.component"; export * from "./fingerprint-dialog/fingerprint-dialog.component"; export * from "./password-callout/password-callout.component"; diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index cd1cab8b422a..4ee725be57fc 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -61,6 +61,7 @@ describe("Collection", () => { const view = await collection.decrypt(); expect(view).toEqual({ + addAccess: false, externalId: "extId", hidePasswords: false, id: "id", diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 86766bdeac6e..f742b283bdaa 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject { readOnly: boolean = null; hidePasswords: boolean = null; manage: boolean = null; + addAccess: boolean = false; assigned: boolean = null; constructor(c?: Collection | CollectionAccessDetailsResponse) {