[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 <vlad@Vlads-MacBook-Pro.local>
Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com>
This commit is contained in:
vlad-trofimov 2026-03-04 07:28:58 -08:00 committed by GitHub
parent 5d8adc08b3
commit f0dbf65dd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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);
});
}