From f0dbf65dd6f5ac8def4ffb43cc3319d74269f165 Mon Sep 17 00:00:00 2001 From: vlad-trofimov <60708931+vlad-trofimov@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:28:58 -0800 Subject: [PATCH] [PM-32765] [PM-32744] Default import destination to My Items when org data owner (#19242) * [PM-32744] Default import destination to My Items when org data ownership policy is enforced * remove feature flag gating, simplify collection selector logic * additional code comments to explain import form behavior --------- Co-authored-by: vlad-trofimov Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com> --- .../src/components/import.component.ts | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 86f5d765d31..03f37ff21c3 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -348,8 +348,13 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { this.isFromAC = true; } + /** + * Initializes the import form for personal vault imports. + * Sets up folder selection for personal vault and collection selection for organizations. + * The targetSelector control dynamically switches between folders (personal vault) and collections (organization vault) based on the vaultSelector value. + */ private async handleImportInit() { - // Filter out the no folder-item from folderViews$ + // Set up observable for user's personal folders (excludes the special "no folder" item) this.folders$ = this.activeUserId$.pipe( switchMap((userId) => { return this.folderService.folderViews$(userId); @@ -357,9 +362,10 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { map((folders) => folders.filter((f) => !!f.id)), ); + // Start with targetSelector disabled - it will be enabled when a vault destination is selected this.formGroup.controls.targetSelector.disable(); - // Retrieve all organizations a user is a member of and has collections they can manage + // Get organizations where the user can import (has manageable collections) const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( combineLatestWith(this.collectionService.decryptedCollections$(userId)), @@ -370,15 +376,18 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { ), ); + // React to vault destination changes (personal vault vs organization selection) combineLatest([this.formGroup.controls.vaultSelector.valueChanges, this.organizations$]) .pipe(takeUntil(this.destroy$)) .subscribe(([value, organizations]) => { + // Set organizationId for org imports, undefined for personal vault this.organizationId = value !== "myVault" ? value : undefined; - if (!this._importBlockedByPolicy) { - this.formGroup.controls.targetSelector.enable(); - } + // Enable targetSelector for both personal vault (folders) and org vault (collections) + // Note: The template switches between showing folders vs collections based on organizationId + this.formGroup.controls.targetSelector.enable(); + // When an organization is selected, load its manageable collections if (value) { this.collections$ = this.collectionService .decryptedCollections$(userId) @@ -391,31 +400,65 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { ); } }); + + // Set initial vault selector to personal vault this.formGroup.controls.vaultSelector.setValue("myVault"); } + /** + * Handles the "Enforce organization data ownership" policy enforcement. + * When this policy is active, users cannot import to their personal vault and must + * select an organization. This method: + * 1. Forces the vault selector to the first available organization [there should only be 1, since My Items requires Single Org policy] + * 2. Auto-selects the user's "My Items" collection as the default import destination + * 3. Disables the entire form if the policy is active but no organizations are available + */ private async handlePolicies() { - combineLatest([ + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + // Create a shared observable combining policy status and available organizations + // This is reused by two subscriptions below to avoid duplicating the combineLatest logic + const policyAndOrgs$ = combineLatest([ this.accountService.activeAccount$.pipe( getUserId, - switchMap((userId) => - this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId), + switchMap((uid) => + this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, uid), ), ), this.organizations$, - ]) - .pipe(takeUntil(this.destroy$)) - .subscribe(([policyApplies, orgs]) => { - this._importBlockedByPolicy = policyApplies; - if (policyApplies && orgs.length == 0) { - this.formGroup.disable(); - } + ]); - // If there are orgs the user has access to import into set - // the default value to the first org in the collection. - if (policyApplies && orgs.length > 0) { - this.formGroup.controls.vaultSelector.setValue(orgs[0].id); - } + // Subscription 1: Handle policy enforcement on vault selection + policyAndOrgs$.pipe(takeUntil(this.destroy$)).subscribe(([policyApplies, orgs]) => { + this._importBlockedByPolicy = policyApplies; + + // If policy applies but user has no organizations they can import to, disable the form + if (policyApplies && orgs.length == 0) { + this.formGroup.disable(); + } + + // If policy applies and user has organizations, force selection of the first org + // (personal vault is hidden when policy is active) + if (policyApplies && orgs.length > 0) { + this.formGroup.controls.vaultSelector.setValue(orgs[0].id); + } + }); + + // Subscription 2: Auto-select "My Items" collection when the "Enforce organization data ownership" policy is active + // It serves as the default landing place for imports, similar to the personal vault. + policyAndOrgs$ + .pipe( + filter(([policyApplies, orgs]) => policyApplies && orgs.length > 0), + switchMap(([, orgs]) => + this.collectionService.defaultUserCollection$(userId, orgs[0].id as OrganizationId), + ), + filter(Boolean), + takeUntil(this.destroy$), + ) + .subscribe((defaultCollection) => { + // Set the targetSelector to the user's My Items collection + // Users can still change this to a different collection if desired + this.formGroup.controls.targetSelector.setValue(defaultCollection); }); }