diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts
index cb8e29bf0..0ce0a1e9c 100644
--- a/src/app/core/constants/nav-items.constant.ts
+++ b/src/app/core/constants/nav-items.constant.ts
@@ -82,6 +82,13 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [
visible: true,
routerLinkActiveOptions: { exact: true },
},
+ {
+ id: 'project-linked-services',
+ label: 'navigation.linkedServices',
+ routerLink: 'links',
+ visible: true,
+ routerLinkActiveOptions: { exact: true },
+ },
{
id: 'project-settings',
label: 'navigation.settings',
diff --git a/src/app/features/files/pages/files/files.component.spec.ts b/src/app/features/files/pages/files/files.component.spec.ts
index 58e9e79bb..2fefa3ed7 100644
--- a/src/app/features/files/pages/files/files.component.spec.ts
+++ b/src/app/features/files/pages/files/files.component.spec.ts
@@ -21,7 +21,7 @@ import {
SubHeaderComponent,
ViewOnlyLinkMessageComponent,
} from '@osf/shared/components';
-import { GoogleFilePickerComponent } from '@osf/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component';
+import { GoogleFilePickerComponent } from '@osf/shared/components/addons/storage-item-selector/google-file-picker/google-file-picker.component';
import { OsfFile } from '@osf/shared/models';
import { CustomConfirmationService, FilesService } from '@osf/shared/services';
diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts
index e0d819218..b437410aa 100644
--- a/src/app/features/files/pages/files/files.component.ts
+++ b/src/app/features/files/pages/files/files.component.ts
@@ -44,7 +44,6 @@ import {
SetSearch,
SetSort,
} from '@osf/features/files/store';
-import { GoogleFilePickerComponent } from '@osf/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component';
import { ALL_SORT_OPTIONS } from '@osf/shared/constants';
import { ResourceType } from '@osf/shared/enums';
import { hasViewOnlyParam, IS_MEDIUM } from '@osf/shared/helpers';
@@ -56,13 +55,8 @@ import {
SubHeaderComponent,
ViewOnlyLinkMessageComponent,
} from '@shared/components';
-import {
- ConfiguredStorageAddonModel,
- FileLabelModel,
- FilesTreeActions,
- OsfFile,
- StorageItemModel,
-} from '@shared/models';
+import { GoogleFilePickerComponent } from '@shared/components/addons/storage-item-selector/google-file-picker/google-file-picker.component';
+import { ConfiguredAddonModel, FileLabelModel, FilesTreeActions, OsfFile, StorageItemModel } from '@shared/models';
import { FilesService } from '@shared/services';
import { CreateFolderDialogComponent, FileBrowserInfoComponent } from '../../components';
@@ -389,7 +383,7 @@ export class FilesComponent {
this.router.navigate([file.guid], { relativeTo: this.activeRoute });
}
- getAddonName(addons: ConfiguredStorageAddonModel[], provider: string): string {
+ getAddonName(addons: ConfiguredAddonModel[], provider: string): string {
if (provider === 'osfstorage') {
return this.translateService.instant('files.storageLocation');
} else {
@@ -407,7 +401,7 @@ export class FilesComponent {
if (googleDrive) {
this.accountId.set(googleDrive.baseAccountId);
this.selectedRootFolder.set({
- itemId: googleDrive.selectedFolderId,
+ itemId: googleDrive.selectedStorageItemId,
});
}
}
diff --git a/src/app/features/files/store/files.model.ts b/src/app/features/files/store/files.model.ts
index 3cb6baac1..895245488 100644
--- a/src/app/features/files/store/files.model.ts
+++ b/src/app/features/files/store/files.model.ts
@@ -1,5 +1,5 @@
import { ContributorModel, OsfFile, ResourceMetadata } from '@shared/models';
-import { ConfiguredStorageAddonModel } from '@shared/models/addons';
+import { ConfiguredAddonModel } from '@shared/models/addons';
import { AsyncStateModel, AsyncStateWithTotalCount } from '@shared/models/store';
import { FileProvider } from '../constants';
@@ -20,7 +20,7 @@ export interface FilesStateModel {
fileRevisions: AsyncStateModel;
tags: AsyncStateModel;
rootFolders: AsyncStateModel;
- configuredStorageAddons: AsyncStateModel;
+ configuredStorageAddons: AsyncStateModel;
isAnonymous: boolean;
}
diff --git a/src/app/features/files/store/files.selectors.ts b/src/app/features/files/store/files.selectors.ts
index 43434bae2..1c0ce291c 100644
--- a/src/app/features/files/store/files.selectors.ts
+++ b/src/app/features/files/store/files.selectors.ts
@@ -1,6 +1,6 @@
import { Selector } from '@ngxs/store';
-import { ConfiguredStorageAddonModel, ContributorModel, OsfFile, ResourceMetadata } from '@shared/models';
+import { ConfiguredAddonModel, ContributorModel, OsfFile, ResourceMetadata } from '@shared/models';
import { OsfFileCustomMetadata, OsfFileRevision } from '../models';
@@ -129,7 +129,7 @@ export class FilesSelectors {
}
@Selector([FilesState])
- static getConfiguredStorageAddons(state: FilesStateModel): ConfiguredStorageAddonModel[] | null {
+ static getConfiguredStorageAddons(state: FilesStateModel): ConfiguredAddonModel[] | null {
return state.configuredStorageAddons.data;
}
diff --git a/src/app/features/project/addons/addons.component.html b/src/app/features/project/addons/addons.component.html
index bf228d176..8ebdf9cfb 100644
--- a/src/app/features/project/addons/addons.component.html
+++ b/src/app/features/project/addons/addons.component.html
@@ -1,4 +1,4 @@
-
+
diff --git a/src/app/features/project/addons/addons.component.ts b/src/app/features/project/addons/addons.component.ts
index 0b6606d1d..43656c346 100644
--- a/src/app/features/project/addons/addons.component.ts
+++ b/src/app/features/project/addons/addons.component.ts
@@ -33,7 +33,9 @@ import {
GetAddonsUserReference,
GetCitationAddons,
GetConfiguredCitationAddons,
+ GetConfiguredLinkAddons,
GetConfiguredStorageAddons,
+ GetLinkAddons,
GetStorageAddons,
} from '@shared/stores/addons';
@@ -60,84 +62,126 @@ import {
export class AddonsComponent implements OnInit {
private route = inject(ActivatedRoute);
private destroyRef = inject(DestroyRef);
- protected readonly tabOptions = ADDON_TAB_OPTIONS;
- protected readonly categoryOptions = ADDON_CATEGORY_OPTIONS;
- protected readonly AddonTabValue = AddonTabValue;
- protected readonly defaultTabValue = AddonTabValue.ALL_ADDONS;
- protected searchControl = new FormControl('');
- protected searchValue = signal('');
- protected selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES);
- protected selectedTab = signal(this.defaultTabValue);
-
- protected currentUser = select(UserSelectors.getCurrentUser);
- protected addonsResourceReference = select(AddonsSelectors.getAddonsResourceReference);
- protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
- protected storageAddons = select(AddonsSelectors.getStorageAddons);
- protected citationAddons = select(AddonsSelectors.getCitationAddons);
- protected configuredStorageAddons = select(AddonsSelectors.getConfiguredStorageAddons);
- protected configuredCitationAddons = select(AddonsSelectors.getConfiguredCitationAddons);
-
- protected isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
- protected isUserReferenceLoading = select(AddonsSelectors.getAddonsUserReferenceLoading);
- protected isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
- protected isStorageAddonsLoading = select(AddonsSelectors.getStorageAddonsLoading);
- protected isCitationAddonsLoading = select(AddonsSelectors.getCitationAddonsLoading);
- protected isConfiguredStorageAddonsLoading = select(AddonsSelectors.getConfiguredStorageAddonsLoading);
- protected isConfiguredCitationAddonsLoading = select(AddonsSelectors.getConfiguredCitationAddonsLoading);
- protected isAddonsLoading = computed(() => {
+ readonly tabOptions = ADDON_TAB_OPTIONS;
+ readonly categoryOptions = ADDON_CATEGORY_OPTIONS;
+ readonly AddonTabValue = AddonTabValue;
+ readonly defaultTabValue = AddonTabValue.ALL_ADDONS;
+ searchControl = new FormControl('');
+ searchValue = signal('');
+ selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES);
+ selectedTab = signal(this.defaultTabValue);
+
+ currentUser = select(UserSelectors.getCurrentUser);
+ addonsResourceReference = select(AddonsSelectors.getAddonsResourceReference);
+ addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
+ storageAddons = select(AddonsSelectors.getStorageAddons);
+ citationAddons = select(AddonsSelectors.getCitationAddons);
+ linkAddons = select(AddonsSelectors.getLinkAddons);
+ configuredStorageAddons = select(AddonsSelectors.getConfiguredStorageAddons);
+ configuredCitationAddons = select(AddonsSelectors.getConfiguredCitationAddons);
+ configuredLinkAddons = select(AddonsSelectors.getConfiguredLinkAddons);
+
+ isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
+ isUserReferenceLoading = select(AddonsSelectors.getAddonsUserReferenceLoading);
+ isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
+ isStorageAddonsLoading = select(AddonsSelectors.getStorageAddonsLoading);
+ isCitationAddonsLoading = select(AddonsSelectors.getCitationAddonsLoading);
+ isConfiguredStorageAddonsLoading = select(AddonsSelectors.getConfiguredStorageAddonsLoading);
+ isConfiguredCitationAddonsLoading = select(AddonsSelectors.getConfiguredCitationAddonsLoading);
+ isConfiguredLinkAddonsLoading = select(AddonsSelectors.getConfiguredLinkAddonsLoading);
+ isAddonsLoading = computed(() => {
return (
this.isStorageAddonsLoading() ||
this.isCitationAddonsLoading() ||
+ this.isLinkAddonsLoading() ||
this.isUserReferenceLoading() ||
this.isCurrentUserLoading()
);
});
- protected isConfiguredAddonsLoading = computed(() => {
+ isConfiguredAddonsLoading = computed(() => {
return (
this.isConfiguredStorageAddonsLoading() ||
this.isConfiguredCitationAddonsLoading() ||
+ this.isConfiguredLinkAddonsLoading() ||
this.isResourceReferenceLoading() ||
this.isCurrentUserLoading()
);
});
- protected actions = createDispatchMap({
+ isLinkAddonsLoading = select(AddonsSelectors.getLinkAddonsLoading);
+
+ currentAddonsLoading = computed(() => {
+ switch (this.selectedCategory()) {
+ case AddonCategory.EXTERNAL_STORAGE_SERVICES:
+ return this.isStorageAddonsLoading();
+ case AddonCategory.EXTERNAL_CITATION_SERVICES:
+ return this.isCitationAddonsLoading();
+ case AddonCategory.EXTERNAL_LINK_SERVICES:
+ return this.isLinkAddonsLoading();
+ default:
+ return this.isStorageAddonsLoading();
+ }
+ });
+
+ actions = createDispatchMap({
getStorageAddons: GetStorageAddons,
getCitationAddons: GetCitationAddons,
+ getLinkAddons: GetLinkAddons,
getConfiguredStorageAddons: GetConfiguredStorageAddons,
getConfiguredCitationAddons: GetConfiguredCitationAddons,
+ getConfiguredLinkAddons: GetConfiguredLinkAddons,
getAddonsUserReference: GetAddonsUserReference,
getAddonsResourceReference: GetAddonsResourceReference,
deleteAuthorizedAddon: DeleteAuthorizedAddon,
clearConfiguredAddons: ClearConfiguredAddons,
});
- protected readonly userReferenceId = computed(() => {
+ readonly userReferenceId = computed(() => {
return this.addonsUserReference()[0]?.id;
});
- protected allConfiguredAddons = computed(() => {
- const authorizedAddons = [...this.configuredStorageAddons(), ...this.configuredCitationAddons()];
+ allConfiguredAddons = computed(() => {
+ const authorizedAddons = [
+ ...this.configuredStorageAddons(),
+ ...this.configuredCitationAddons(),
+ ...this.configuredLinkAddons(),
+ ];
const searchValue = this.searchValue().toLowerCase();
return authorizedAddons.filter((card) => card.displayName.toLowerCase().includes(searchValue));
});
- protected resourceReferenceId = computed(() => {
+ resourceReferenceId = computed(() => {
return this.addonsResourceReference()[0]?.id;
});
- protected currentAction = computed(() =>
- this.selectedCategory() === AddonCategory.EXTERNAL_STORAGE_SERVICES
- ? this.actions.getStorageAddons
- : this.actions.getCitationAddons
- );
+ currentAction = computed(() => {
+ switch (this.selectedCategory()) {
+ case AddonCategory.EXTERNAL_STORAGE_SERVICES:
+ return this.actions.getStorageAddons;
+ case AddonCategory.EXTERNAL_CITATION_SERVICES:
+ return this.actions.getCitationAddons;
+ case AddonCategory.EXTERNAL_LINK_SERVICES:
+ return this.actions.getLinkAddons;
+ default:
+ return this.actions.getStorageAddons;
+ }
+ });
- protected currentAddonsState = computed(() =>
- this.selectedCategory() === AddonCategory.EXTERNAL_STORAGE_SERVICES ? this.storageAddons() : this.citationAddons()
- );
+ currentAddonsState = computed(() => {
+ switch (this.selectedCategory()) {
+ case AddonCategory.EXTERNAL_STORAGE_SERVICES:
+ return this.storageAddons();
+ case AddonCategory.EXTERNAL_CITATION_SERVICES:
+ return this.citationAddons();
+ case AddonCategory.EXTERNAL_LINK_SERVICES:
+ return this.linkAddons();
+ default:
+ return this.storageAddons();
+ }
+ });
- protected filteredAddonCards = computed(() => {
+ filteredAddonCards = computed(() => {
const searchValue = this.searchValue().toLowerCase();
return this.currentAddonsState().filter(
(card) =>
@@ -146,7 +190,7 @@ export class AddonsComponent implements OnInit {
);
});
- protected onCategoryChange(value: Primitive): void {
+ onCategoryChange(value: Primitive): void {
if (typeof value === 'string') {
this.selectedCategory.set(value);
}
@@ -163,8 +207,9 @@ export class AddonsComponent implements OnInit {
if (this.currentUser()) {
const action = this.currentAction();
const addons = this.currentAddonsState();
+ const isLoading = this.currentAddonsLoading();
- if (!addons?.length) {
+ if (!addons?.length && !isLoading) {
action();
}
}
@@ -199,5 +244,6 @@ export class AddonsComponent implements OnInit {
private fetchAllConfiguredAddons(resourceReferenceId: string): void {
this.actions.getConfiguredStorageAddons(resourceReferenceId);
this.actions.getConfiguredCitationAddons(resourceReferenceId);
+ this.actions.getConfiguredLinkAddons(resourceReferenceId);
}
}
diff --git a/src/app/features/project/addons/components/configure-addon/configure-addon.component.html b/src/app/features/project/addons/components/configure-addon/configure-addon.component.html
index 9e2993dd6..977fdcf37 100644
--- a/src/app/features/project/addons/components/configure-addon/configure-addon.component.html
+++ b/src/app/features/project/addons/components/configure-addon/configure-addon.component.html
@@ -5,7 +5,7 @@
- {{ 'settings.addons.connectAddon.configure' | translate }} {{ addon()?.externalServiceName }}
+ {{ 'settings.addons.connectAddon.configure' | translate }} {{ addonServiceName() }}
>
- @if (selectedFolder()) {
+ @if (selectedStorageItem()) {
@@ -41,8 +41,8 @@
- {{ 'settings.addons.configureAddon.selectedFolder' | translate }}
- {{ selectedFolder()?.itemName }}
+ {{ selectedItemLabel() | translate }}
+ {{ selectedStorageItem()?.itemName }}
@@ -54,7 +54,7 @@
- {{ 'settings.addons.connectAddon.configure' | translate }} {{ addon()?.externalServiceName }}
+ {{ 'settings.addons.connectAddon.configure' | translate }} {{ addonServiceName() }}
(keydown.enter)="toggleEditMode()"
>
-
diff --git a/src/app/features/project/addons/components/configure-addon/configure-addon.component.spec.ts b/src/app/features/project/addons/components/configure-addon/configure-addon.component.spec.ts
index 1b041160f..92fa799fd 100644
--- a/src/app/features/project/addons/components/configure-addon/configure-addon.component.spec.ts
+++ b/src/app/features/project/addons/components/configure-addon/configure-addon.component.spec.ts
@@ -94,7 +94,7 @@ describe('Component: Configure Addon', () => {
expect(component.baseUrl()).toBe('/project/abc123');
expect(component.resourceUri()).toBe('https://staging4.osf.io/mocked-id');
expect(component.addonTypeString()).toBe('storage');
- expect(component.selectedRootFolderId()).toBeUndefined();
+ expect(component.selectedStorageItemId).toBeDefined();
expect(component.accountNameControl.value).toBeUndefined();
expect(component.isGoogleDrive()).toBeFalsy();
});
diff --git a/src/app/features/project/addons/components/configure-addon/configure-addon.component.ts b/src/app/features/project/addons/components/configure-addon/configure-addon.component.ts
index 20d622702..52b57b7fd 100644
--- a/src/app/features/project/addons/components/configure-addon/configure-addon.component.ts
+++ b/src/app/features/project/addons/components/configure-addon/configure-addon.component.ts
@@ -24,13 +24,15 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { OperationNames } from '@osf/features/project/addons/enums';
import { getAddonTypeString } from '@osf/shared/helpers';
import { SubHeaderComponent } from '@shared/components';
-import { FolderSelectorComponent } from '@shared/components/addons/folder-selector/folder-selector.component';
-import { AddonModel, ConfiguredStorageAddonModel } from '@shared/models';
+import { StorageItemSelectorComponent } from '@shared/components/addons';
+import { AddonServiceNames, AddonType } from '@shared/enums';
+import { AddonModel, ConfiguredAddonModel } from '@shared/models';
import { AddonDialogService, AddonFormService, AddonOperationInvocationService, ToastService } from '@shared/services';
import {
AddonsSelectors,
ClearOperationInvocations,
CreateAddonOperationInvocation,
+ GetLinkAddons,
UpdateConfiguredAddon,
} from '@shared/stores/addons';
@@ -48,7 +50,8 @@ import { environment } from 'src/environments/environment';
FormsModule,
Skeleton,
BreadcrumbModule,
- FolderSelectorComponent,
+ StorageItemSelectorComponent,
+ StorageItemSelectorComponent,
],
templateUrl: './configure-addon.component.html',
styleUrl: './configure-addon.component.scss',
@@ -63,36 +66,25 @@ export class ConfigureAddonComponent implements OnInit {
private addonDialogService = inject(AddonDialogService);
private addonFormService = inject(AddonFormService);
private operationInvocationService = inject(AddonOperationInvocationService);
- /**
- * Injected NGXS store used to access and dispatch state actions and selectors.
- */
private store = inject(Store);
-
- /**
- * Form control for capturing or displaying the user’s selected account name.
- */
- public accountNameControl = new FormControl('');
- /**
- * Signal representing the currently selected `Addon` from the list of available storage addons.
- * This value updates reactively as the selection changes.
- */
- public storageAddon = signal(null);
- /**
- * Signal representing the currently selected and configured storage addon model.
- * This may be `null` if no addon has been configured.
- */
- public addon = signal(null);
-
- public readonly isGoogleDrive = computed(() => {
+ accountNameControl = new FormControl('');
+ storageAddon = signal(null);
+ addon = signal(null);
+ readonly isGoogleDrive = computed(() => {
return this.storageAddon()?.wbKey === 'googledrive';
});
-
- protected isEditMode = signal(false);
- public selectedRootFolderId = signal('');
- protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
- public operationInvocation = select(AddonsSelectors.getOperationInvocation);
- protected selectedFolderOperationInvocation = select(AddonsSelectors.getSelectedFolderOperationInvocation);
- protected selectedFolder = select(AddonsSelectors.getSelectedFolder);
+ isEditMode = signal(false);
+ selectedStorageItemId = signal('');
+ selectedStorageItemUrl = signal('');
+ selectedResourceType = signal('');
+ addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
+ operationInvocation = select(AddonsSelectors.getOperationInvocation);
+ linkAddons = select(AddonsSelectors.getLinkAddons);
+ selectedStorageItem = select(AddonsSelectors.getSelectedStorageItem);
+
+ addonServiceName = computed(() => {
+ return AddonServiceNames[this.addon()?.externalServiceName as keyof typeof AddonServiceNames];
+ });
readonly baseUrl = computed(() => {
const currentUrl = this.router.url;
@@ -105,10 +97,24 @@ export class ConfigureAddonComponent implements OnInit {
readonly addonTypeString = computed(() => {
return getAddonTypeString(this.addon());
});
- protected readonly actions = createDispatchMap({
+ readonly selectedItemLabel = computed(() => {
+ const addonType = this.addonTypeString();
+ return addonType === AddonType.LINK
+ ? 'settings.addons.configureAddon.linkedItem'
+ : 'settings.addons.configureAddon.selectedFolder';
+ });
+ readonly supportedResourceTypes = computed(() => {
+ if (this.linkAddons().length && this.addonTypeString() === AddonType.LINK) {
+ const addon = this.linkAddons().find((a) => this.addon()?.externalServiceName === a.externalServiceName);
+ return addon?.supportedResourceTypes || [];
+ }
+ return [];
+ });
+ readonly actions = createDispatchMap({
createAddonOperationInvocation: CreateAddonOperationInvocation,
updateConfiguredAddon: UpdateConfiguredAddon,
clearOperationInvocations: ClearOperationInvocations,
+ getLinkAddons: GetLinkAddons,
});
constructor() {
@@ -119,12 +125,18 @@ export class ConfigureAddonComponent implements OnInit {
this.actions.clearOperationInvocations();
});
});
+
+ effect(() => {
+ if (this.addonTypeString() === AddonType.LINK) {
+ this.actions.getLinkAddons();
+ }
+ });
}
private initializeAddon(): void {
// TODO this should be reviewed to have the addon be retrieved from the store
// I have limited my testing because it will create a false/positive test based on the required data
- const addon = this.router.getCurrentNavigation()?.extras.state?.['addon'] as ConfiguredStorageAddonModel;
+ const addon = this.router.getCurrentNavigation()?.extras.state?.['addon'] as ConfiguredAddonModel;
if (addon) {
this.storageAddon.set(
@@ -132,14 +144,16 @@ export class ConfigureAddonComponent implements OnInit {
);
this.addon.set(addon);
- this.selectedRootFolderId.set(addon.selectedFolderId);
+ this.selectedStorageItemId.set(addon.selectedStorageItemId);
+ this.selectedStorageItemUrl.set(addon.targetUrl || '');
+ this.selectedResourceType.set(addon.resourceType || '');
this.accountNameControl.setValue(addon.displayName);
} else {
this.router.navigate([`${this.baseUrl()}/addons`]);
}
}
- protected handleCreateOperationInvocation(operationName: OperationNames, folderId: string): void {
+ handleCreateOperationInvocation(operationName: OperationNames, folderId: string): void {
const addon = this.addon();
if (!addon) return;
@@ -149,17 +163,17 @@ export class ConfigureAddonComponent implements OnInit {
}
ngOnInit(): void {
- this.handleCreateOperationInvocation(OperationNames.GET_ITEM_INFO, this.selectedRootFolderId());
+ this.handleCreateOperationInvocation(OperationNames.GET_ITEM_INFO, this.selectedStorageItemId());
}
- protected handleDisconnectAccount(): void {
+ handleDisconnectAccount(): void {
const currentAddon = this.addon();
if (!currentAddon) return;
this.openDisconnectDialog(currentAddon);
}
- private openDisconnectDialog(addon: ConfiguredStorageAddonModel): void {
+ private openDisconnectDialog(addon: ConfiguredAddonModel): void {
const dialogRef = this.addonDialogService.openDisconnectDialog(addon);
dialogRef.subscribe((result) => {
@@ -172,19 +186,16 @@ export class ConfigureAddonComponent implements OnInit {
});
}
- protected toggleEditMode(): void {
- const operationResult = this.selectedFolderOperationInvocation()?.operationResult[0];
- const hasRootCandidates = operationResult?.mayContainRootCandidates ?? false;
- const itemId = operationResult?.itemId || '/';
+ toggleEditMode(): void {
+ if (!this.isEditMode()) {
+ this.resetConfigurationForm();
+ }
- this.handleCreateOperationInvocation(
- hasRootCandidates ? OperationNames.LIST_CHILD_ITEMS : OperationNames.GET_ITEM_INFO,
- itemId
- );
+ this.handleCreateOperationInvocation(OperationNames.LIST_ROOT_ITEMS, this.selectedStorageItemId());
this.isEditMode.set(!this.isEditMode());
}
- protected handleUpdateAddonConfiguration(): void {
+ handleUpdateAddonConfiguration(): void {
const currentAddon = this.addon();
if (!currentAddon) return;
@@ -193,17 +204,29 @@ export class ConfigureAddonComponent implements OnInit {
this.addonsUserReference()[0].id || '',
this.resourceUri(),
this.accountNameControl.value || '',
- this.selectedRootFolderId() || '',
- this.addonTypeString()
+ this.selectedStorageItemId() || '',
+ this.addonTypeString(),
+ this.selectedResourceType(),
+ this.selectedStorageItemUrl()
);
this.actions.updateConfiguredAddon(payload, this.addonTypeString(), currentAddon.id).subscribe({
complete: () => {
this.router.navigate([`${this.baseUrl()}/addons`]);
this.toastService.showSuccess('settings.addons.toast.updateSuccess', {
- addonName: currentAddon.externalServiceName,
+ addonName: this.addonServiceName(),
});
},
});
}
+
+ private resetConfigurationForm(): void {
+ this.selectedStorageItemId.set('');
+ this.selectedStorageItemUrl.set('');
+ const currentAddon = this.addon();
+ if (currentAddon) {
+ this.selectedResourceType.set(currentAddon.resourceType || '');
+ this.accountNameControl.setValue(currentAddon.displayName);
+ }
+ }
}
diff --git a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.html b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.html
index c8a2aaa9b..fcf990484 100644
--- a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.html
+++ b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.html
@@ -18,9 +18,11 @@
{{ 'settings.addons.connectAddon.termsDescription' | translate }}
-
- {{ 'settings.addons.connectAddon.storageDescription' | translate }}
-
+ @if (addonTypeString() === AddonType.STORAGE) {
+
+ {{ 'settings.addons.connectAddon.storageDescription' | translate }}
+
+ }
@@ -113,17 +115,21 @@ {{ 'settings.addons.connectAddon.chooseExistingAccount' | trans
{{ 'settings.addons.connectAddon.configure' | translate }} {{ addon()?.displayName }}
-
diff --git a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.ts b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.ts
index f2c6cf70c..ad3b7ecfe 100644
--- a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.ts
+++ b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.ts
@@ -16,14 +16,15 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { OperationNames } from '@osf/features/project/addons/enums';
import { AddonConfigMap } from '@osf/features/project/addons/utils';
import { SubHeaderComponent } from '@osf/shared/components';
-import { ProjectAddonsStepperValue } from '@osf/shared/enums';
+import { AddonType, ProjectAddonsStepperValue } from '@osf/shared/enums';
import { getAddonTypeString } from '@osf/shared/helpers';
import { AuthorizedAccountModel } from '@osf/shared/models/addons/authorized-account.model';
import {
AddonSetupAccountFormComponent,
AddonTermsComponent,
- FolderSelectorComponent,
+ StorageItemSelectorComponent,
} from '@shared/components/addons';
+import { AddonServiceNames } from '@shared/enums';
import { AddonModel, AddonTerm, AuthorizedAddonRequestJsonApi } from '@shared/models';
import { AddonDialogService, AddonFormService, AddonOperationInvocationService, ToastService } from '@shared/services';
import {
@@ -32,6 +33,7 @@ import {
CreateAuthorizedAddon,
CreateConfiguredAddon,
GetAuthorizedCitationAddons,
+ GetAuthorizedLinkAddons,
GetAuthorizedStorageAddons,
UpdateAuthorizedAddon,
UpdateConfiguredAddon,
@@ -53,7 +55,7 @@ import { environment } from 'src/environments/environment';
ReactiveFormsModule,
TranslatePipe,
RadioButtonModule,
- FolderSelectorComponent,
+ StorageItemSelectorComponent,
AddonTermsComponent,
AddonSetupAccountFormComponent,
DialogModule,
@@ -71,35 +73,48 @@ export class ConnectConfiguredAddonComponent {
private operationInvocationService = inject(AddonOperationInvocationService);
private router = inject(Router);
private route = inject(ActivatedRoute);
- protected readonly AddonStepperValue = ProjectAddonsStepperValue;
- protected readonly stepper = viewChild(Stepper);
- protected accountNameControl = new FormControl('');
- protected terms = signal([]);
- protected addon = signal(null);
- protected addonAuthUrl = signal('/settings/addons');
- protected currentAuthorizedAddonAccounts = signal([]);
- protected chosenAccountId = signal('');
- protected chosenAccountName = signal('');
- protected selectedRootFolderId = signal('');
private selectedAccount = signal({} as AuthorizedAccountModel);
- public readonly isGoogleDrive = computed(() => {
+ readonly isGoogleDrive = computed(() => {
return this.selectedAccount()?.externalServiceName === 'googledrive';
});
+ readonly AddonStepperValue = ProjectAddonsStepperValue;
+ readonly AddonType = AddonType;
+ readonly stepper = viewChild(Stepper);
+ accountNameControl = new FormControl('');
+ terms = signal([]);
+ addon = signal(null);
+ addonAuthUrl = signal('/settings/addons');
+ currentAuthorizedAddonAccounts = signal([]);
+ chosenAccountId = signal('');
+ chosenAccountName = signal('');
+ selectedStorageItemId = signal('');
+ selectedStorageItemUrl = signal('');
+ selectedResourceType = signal('');
+
+ addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
+ createdAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedAuthorizedAddon);
+ createdConfiguredAddon = select(AddonsSelectors.getCreatedOrUpdatedConfiguredAddon);
+ authorizedStorageAddons = select(AddonsSelectors.getAuthorizedStorageAddons);
+ authorizedCitationAddons = select(AddonsSelectors.getAuthorizedCitationAddons);
+ authorizedLinkAddons = select(AddonsSelectors.getAuthorizedLinkAddons);
+ operationInvocation = select(AddonsSelectors.getOperationInvocation);
+
+ isAuthorizedStorageAddonsLoading = select(AddonsSelectors.getAuthorizedStorageAddonsLoading);
+ isAuthorizedCitationAddonsLoading = select(AddonsSelectors.getAuthorizedCitationAddonsLoading);
+ isAuthorizedLinkAddonsLoading = select(AddonsSelectors.getAuthorizedLinkAddonsLoading);
+ isAuthorizedAddonsLoading = computed(() => {
+ return (
+ this.isAuthorizedStorageAddonsLoading() ||
+ this.isAuthorizedCitationAddonsLoading() ||
+ this.isAuthorizedLinkAddonsLoading()
+ );
+ });
+ isCreatingAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedStorageAddonSubmitting);
- protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
- protected createdAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedAuthorizedAddon);
- protected createdConfiguredAddon = select(AddonsSelectors.getCreatedOrUpdatedConfiguredAddon);
- protected authorizedStorageAddons = select(AddonsSelectors.getAuthorizedStorageAddons);
- protected authorizedCitationAddons = select(AddonsSelectors.getAuthorizedCitationAddons);
- protected operationInvocation = select(AddonsSelectors.getOperationInvocation);
-
- protected isAuthorizedStorageAddonsLoading = select(AddonsSelectors.getAuthorizedStorageAddonsLoading);
- protected isAuthorizedCitationAddonsLoading = select(AddonsSelectors.getAuthorizedCitationAddonsLoading);
- protected isCreatingAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedStorageAddonSubmitting);
-
- protected actions = createDispatchMap({
+ actions = createDispatchMap({
getAuthorizedStorageAddons: GetAuthorizedStorageAddons,
getAuthorizedCitationAddons: GetAuthorizedCitationAddons,
+ getAuthorizedLinkAddons: GetAuthorizedLinkAddons,
createAuthorizedAddon: CreateAuthorizedAddon,
createConfiguredAddon: CreateConfiguredAddon,
updateConfiguredAddon: UpdateConfiguredAddon,
@@ -107,30 +122,35 @@ export class ConnectConfiguredAddonComponent {
createAddonOperationInvocation: CreateAddonOperationInvocation,
});
- protected readonly userReferenceId = computed(() => {
+ readonly userReferenceId = computed(() => {
return this.addonsUserReference()[0]?.id;
});
- protected loginOrChooseAccountText = computed(() => {
+ loginOrChooseAccountText = computed(() => {
return this.translateService.instant('settings.addons.connectAddon.loginToOrSelectAccount', {
addonName: this.addon()?.displayName,
});
});
- protected resourceUri = computed(() => {
+ resourceUri = computed(() => {
const id = this.route.parent?.parent?.snapshot.params['id'];
return `${environment.webUrl}/${id}`;
});
- protected addonTypeString = computed(() => {
+ addonTypeString = computed(() => {
return getAddonTypeString(this.addon());
});
- protected readonly baseUrl = computed(() => {
+ readonly baseUrl = computed(() => {
const currentUrl = this.router.url;
return currentUrl.split('/addons')[0];
});
+ readonly supportedResourceTypes = computed(() => {
+ const addon = this.addon();
+ return addon && 'supportedResourceTypes' in addon ? addon.supportedResourceTypes || [] : [];
+ });
+
constructor() {
const addon = this.router.getCurrentNavigation()?.extras.state?.['addon'] as AddonModel | AuthorizedAccountModel;
if (!addon) {
@@ -139,7 +159,7 @@ export class ConnectConfiguredAddonComponent {
this.addon.set(addon);
}
- protected handleCreateConfiguredAddon() {
+ handleCreateConfiguredAddon() {
const addon = this.addon();
this.selectedAccount.set(
this.currentAuthorizedAddonAccounts().find((account) => account.id === this.chosenAccountId()) ||
@@ -153,8 +173,10 @@ export class ConnectConfiguredAddonComponent {
this.userReferenceId(),
this.resourceUri(),
this.accountNameControl.value || '',
- this.selectedRootFolderId(),
- this.addonTypeString()
+ this.selectedStorageItemId(),
+ this.addonTypeString(),
+ this.selectedResourceType(),
+ this.selectedStorageItemUrl()
);
this.actions.createConfiguredAddon(payload, this.addonTypeString()).subscribe({
@@ -163,29 +185,34 @@ export class ConnectConfiguredAddonComponent {
if (createdAddon) {
this.router.navigate([`${this.baseUrl()}/addons`]);
this.toastService.showSuccess('settings.addons.toast.createSuccess', {
- addonName: addon.externalServiceName,
+ addonName: AddonServiceNames[addon.externalServiceName as keyof typeof AddonServiceNames],
});
}
},
});
}
- protected handleCreateAuthorizedAddon(payload: AuthorizedAddonRequestJsonApi): void {
+ handleCreateAuthorizedAddon(payload: AuthorizedAddonRequestJsonApi): void {
if (!this.addon()) return;
this.actions.createAuthorizedAddon(payload, this.addonTypeString()).subscribe({
complete: () => {
- const createdAddon = this.createdAuthorizedAddon();
- if (createdAddon) {
- this.addonAuthUrl.set(createdAddon.attributes.auth_url);
- window.open(createdAddon.attributes.auth_url, '_blank');
+ const addon = this.createdAuthorizedAddon();
+ if (addon?.authUrl) {
+ this.addonAuthUrl.set(addon.authUrl);
+ window.open(addon.authUrl, '_blank');
this.stepper()?.value.set(ProjectAddonsStepperValue.AUTH);
+ } else {
+ this.router.navigate([`${this.baseUrl()}/addons`]);
+ this.toastService.showSuccess('settings.addons.toast.createSuccess', {
+ addonName: AddonServiceNames[addon?.externalServiceName as keyof typeof AddonServiceNames],
+ });
}
},
});
}
- protected handleConfirmAccountConnection(): void {
+ handleConfirmAccountConnection(): void {
this.selectedAccount.set(
this.currentAuthorizedAddonAccounts().find((account) => account.id === this.chosenAccountId()) ||
({} as AuthorizedAccountModel)
@@ -200,11 +227,12 @@ export class ConnectConfiguredAddonComponent {
this.stepper()?.value.set(ProjectAddonsStepperValue.CONFIGURE_ROOT_FOLDER);
this.chosenAccountName.set(this.selectedAccount().displayName);
this.accountNameControl.setValue(this.selectedAccount().displayName);
+ this.resetConfigurationForm();
}
});
}
- protected handleAuthorizedAccountsPresenceCheck() {
+ handleAuthorizedAccountsPresenceCheck() {
const requiredData = this.getDataForAccountCheck();
if (!requiredData) return;
@@ -234,14 +262,18 @@ export class ConnectConfiguredAddonComponent {
private getAddonConfig(addonType: string, referenceId: string) {
const addonConfigMap: AddonConfigMap = {
- storage: {
+ [AddonType.STORAGE]: {
getAddons: () => this.actions.getAuthorizedStorageAddons(referenceId),
getAuthorizedAddons: () => this.authorizedStorageAddons(),
},
- citation: {
+ [AddonType.CITATION]: {
getAddons: () => this.actions.getAuthorizedCitationAddons(referenceId),
getAuthorizedAddons: () => this.authorizedCitationAddons(),
},
+ [AddonType.LINK]: {
+ getAddons: () => this.actions.getAuthorizedLinkAddons(referenceId),
+ getAuthorizedAddons: () => this.authorizedLinkAddons(),
+ },
};
return addonConfigMap[addonType] || null;
@@ -273,7 +305,7 @@ export class ConnectConfiguredAddonComponent {
return authorizedAddons.filter((addon) => addon.externalServiceName === currentAddon.externalServiceName);
}
- protected handleCreateOperationInvocation(operationName: OperationNames, itemId: string): void {
+ handleCreateOperationInvocation(operationName: OperationNames, itemId: string): void {
const selectedAccount = this.currentAuthorizedAddonAccounts().find(
(account) => account.id === this.chosenAccountId()
);
@@ -288,4 +320,15 @@ export class ConnectConfiguredAddonComponent {
this.actions.createAddonOperationInvocation(payload);
}
+
+ handleNavigateToAccountSelection(): void {
+ this.resetConfigurationForm();
+ this.stepper()?.value.set(ProjectAddonsStepperValue.CHOOSE_ACCOUNT);
+ }
+
+ private resetConfigurationForm(): void {
+ this.selectedStorageItemId.set('');
+ this.selectedStorageItemUrl.set('');
+ this.selectedResourceType.set('');
+ }
}
diff --git a/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html b/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html
index e665b6e3d..e4447ba44 100644
--- a/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html
+++ b/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html
@@ -4,7 +4,7 @@
{{ 'settings.addons.configureAddon.account' | translate }} {{ addon.displayName }}
- {{ 'settings.addons.configureAddon.selectedFolder' | translate }}
+ {{ selectedItemLabel() | translate }}
{{ selectedFolder()?.itemName }}
diff --git a/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts b/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts
index 2821f7432..ad0cc5ead 100644
--- a/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts
+++ b/src/app/features/project/addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts
@@ -5,8 +5,10 @@ import { TranslatePipe } from '@ngx-translate/core';
import { Button } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
-import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
+import { getAddonTypeString } from '@osf/shared/helpers';
+import { AddonType } from '@shared/enums';
import { AddonsSelectors, DeleteConfiguredAddon } from '@shared/stores/addons';
@Component({
@@ -18,16 +20,22 @@ import { AddonsSelectors, DeleteConfiguredAddon } from '@shared/stores/addons';
})
export class DisconnectAddonModalComponent {
private dialogConfig = inject(DynamicDialogConfig);
- protected dialogRef = inject(DynamicDialogRef);
- protected addon = this.dialogConfig.data.addon;
- protected dialogMessage = this.dialogConfig.data.message || '';
- protected isSubmitting = select(AddonsSelectors.getDeleteStorageAddonSubmitting);
- protected selectedFolder = select(AddonsSelectors.getSelectedFolder);
- protected actions = createDispatchMap({
+ dialogRef = inject(DynamicDialogRef);
+ addon = this.dialogConfig.data.addon;
+ dialogMessage = this.dialogConfig.data.message || '';
+ isSubmitting = select(AddonsSelectors.getDeleteStorageAddonSubmitting);
+ selectedFolder = select(AddonsSelectors.getSelectedStorageItem);
+ selectedItemLabel = computed(() => {
+ const addonType = getAddonTypeString(this.addon);
+ return addonType === AddonType.LINK
+ ? 'settings.addons.configureAddon.linkedItem'
+ : 'settings.addons.configureAddon.selectedFolder';
+ });
+ actions = createDispatchMap({
deleteConfiguredAddon: DeleteConfiguredAddon,
});
- protected handleDisconnectAddonAccount(): void {
+ handleDisconnectAddonAccount(): void {
if (!this.addon) return;
this.actions.deleteConfiguredAddon(this.addon.id, this.addon.type).subscribe({
diff --git a/src/app/features/project/linked-services/linked-services.component.html b/src/app/features/project/linked-services/linked-services.component.html
new file mode 100644
index 000000000..a36358f21
--- /dev/null
+++ b/src/app/features/project/linked-services/linked-services.component.html
@@ -0,0 +1,53 @@
+
+
+ @if (isLoading()) {
+
+
+
+ } @else if (convertedConfiguredLinkAddons().length) {
+
+
+
+
+ {{ 'project.linkedServices.table.linkedService' | translate }}
+ |
+
+ {{ 'project.linkedServices.table.displayName' | translate }}
+ |
+
+ {{ 'project.linkedServices.table.resourceType' | translate }}
+ |
+
+ {{ 'common.buttons.edit' | translate }}
+ |
+
+
+
+
+
+ {{ item.serviceName }} |
+ {{ item.displayName }} |
+ {{ item.convertedResourceType }} |
+
+
+ {{ 'project.linkedServices.table.link' | translate }}
+
+ |
+
+
+
+ } @else {
+
+
+ {{ 'project.linkedServices.noLinkedServices' | translate }}
+
+
+ {{ 'project.linkedServices.redirectMessage' | translate }}
+
+ {{ 'project.linkedServices.addonsLink' | translate }}
+
+ {{ 'project.linkedServices.redirectMessageSuffix' | translate }}
+
+
+ }
+
diff --git a/src/app/features/project/linked-services/linked-services.component.scss b/src/app/features/project/linked-services/linked-services.component.scss
new file mode 100644
index 000000000..da0c027b5
--- /dev/null
+++ b/src/app/features/project/linked-services/linked-services.component.scss
@@ -0,0 +1,5 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
diff --git a/src/app/features/project/linked-services/linked-services.component.spec.ts b/src/app/features/project/linked-services/linked-services.component.spec.ts
new file mode 100644
index 000000000..14c4cac4d
--- /dev/null
+++ b/src/app/features/project/linked-services/linked-services.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkedServicesComponent } from './linked-services.component';
+
+describe.skip('LinkedServicesComponent', () => {
+ let component: LinkedServicesComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LinkedServicesComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(LinkedServicesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/linked-services/linked-services.component.ts b/src/app/features/project/linked-services/linked-services.component.ts
new file mode 100644
index 000000000..ea280a770
--- /dev/null
+++ b/src/app/features/project/linked-services/linked-services.component.ts
@@ -0,0 +1,69 @@
+import { createDispatchMap, select } from '@ngxs/store';
+
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { TableModule } from 'primeng/table';
+
+import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit } from '@angular/core';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+
+import { UserSelectors } from '@core/store/user';
+import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components';
+import { AddonServiceNames } from '@shared/enums';
+import { convertCamelCaseToNormal } from '@shared/helpers';
+import { AddonsSelectors, GetAddonsResourceReference, GetConfiguredLinkAddons } from '@shared/stores';
+
+@Component({
+ selector: 'osf-linked-services',
+ imports: [SubHeaderComponent, TranslatePipe, TableModule, LoadingSpinnerComponent, RouterLink],
+ templateUrl: './linked-services.component.html',
+ styleUrl: './linked-services.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class LinkedServicesComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+
+ currentUser = select(UserSelectors.getCurrentUser);
+ addonsResourceReference = select(AddonsSelectors.getAddonsResourceReference);
+ configuredLinkAddons = select(AddonsSelectors.getConfiguredLinkAddons);
+ isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
+ isConfiguredLinkAddonsLoading = select(AddonsSelectors.getConfiguredLinkAddonsLoading);
+ isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
+
+ isLoading = computed(() => {
+ return this.isConfiguredLinkAddonsLoading() || this.isResourceReferenceLoading() || this.isCurrentUserLoading();
+ });
+
+ resourceReferenceId = computed(() => this.addonsResourceReference()[0]?.id);
+
+ convertedConfiguredLinkAddons = computed(() => {
+ return this.configuredLinkAddons().map((item) => ({
+ ...item,
+ serviceName:
+ AddonServiceNames[item.externalServiceName as keyof typeof AddonServiceNames] || item.externalServiceName,
+ convertedResourceType: convertCamelCaseToNormal(item.resourceType || ''),
+ }));
+ });
+
+ actions = createDispatchMap({
+ getConfiguredLinkAddons: GetConfiguredLinkAddons,
+ getAddonsResourceReference: GetAddonsResourceReference,
+ });
+
+ constructor() {
+ effect(() => {
+ const resourceReferenceId = this.resourceReferenceId();
+ if (resourceReferenceId) {
+ this.actions.getConfiguredLinkAddons(resourceReferenceId);
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ const projectId = this.route.parent?.parent?.snapshot.params['id'];
+
+ if (projectId && !this.addonsResourceReference().length) {
+ this.actions.getAddonsResourceReference(projectId);
+ }
+ }
+}
diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts
index 6fa3518d3..e54bcfec3 100644
--- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts
+++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts
@@ -32,7 +32,7 @@ import {
import { FilesTreeComponent, SelectComponent } from '@osf/shared/components';
import { Primitive } from '@osf/shared/helpers';
import {
- ConfiguredStorageAddonModel,
+ ConfiguredAddonModel,
FileLabelModel,
FilesTreeActions,
NodeShortInfoModel,
@@ -198,7 +198,7 @@ export class FilesWidgetComponent {
}, []);
}
- private getAddonName(addons: ConfiguredStorageAddonModel[], provider: string): string {
+ private getAddonName(addons: ConfiguredAddonModel[], provider: string): string {
if (provider === FileProvider.OsfStorage) {
return this.osfStorageLabel;
} else {
diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html
index c2d44da4c..68da00521 100644
--- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html
+++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html
@@ -34,15 +34,20 @@
-
+
+
{{ 'common.labels.contributors' | translate }}:
@for (contributor of linkedResource.contributors; track contributor.id) {
}
+
+
diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts b/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts
index bc3d43d62..9a3e1e76b 100644
--- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts
+++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts
@@ -31,9 +31,9 @@ export class LinkedResourcesComponent {
isCollectionsRoute = input(false);
canWrite = input.required();
- protected linkedResources = select(NodeLinksSelectors.getLinkedResources);
- protected isLinkedResourcesLoading = select(NodeLinksSelectors.getLinkedResourcesLoading);
- protected isMedium = toSignal(inject(IS_MEDIUM));
+ linkedResources = select(NodeLinksSelectors.getLinkedResources);
+ isLinkedResourcesLoading = select(NodeLinksSelectors.getLinkedResourcesLoading);
+ isMedium = toSignal(inject(IS_MEDIUM));
openLinkProjectModal() {
const dialogWidth = this.isMedium() ? '850px' : '95vw';
diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts
index 8a018747d..94c3d3947 100644
--- a/src/app/features/project/project.routes.ts
+++ b/src/app/features/project/project.routes.ts
@@ -100,6 +100,12 @@ export const projectRoutes: Routes = [
canActivate: [viewOnlyGuard],
loadChildren: () => import('../project/addons/addons.routes').then((mod) => mod.addonsRoutes),
},
+ {
+ path: 'links',
+ canActivate: [viewOnlyGuard],
+ loadComponent: () =>
+ import('../project/linked-services/linked-services.component').then((mod) => mod.LinkedServicesComponent),
+ },
],
},
];
diff --git a/src/app/features/settings/addons/addons.component.ts b/src/app/features/settings/addons/addons.component.ts
index 2b47a827b..7c07173f8 100644
--- a/src/app/features/settings/addons/addons.component.ts
+++ b/src/app/features/settings/addons/addons.component.ts
@@ -26,8 +26,10 @@ import {
DeleteAuthorizedAddon,
GetAddonsUserReference,
GetAuthorizedCitationAddons,
+ GetAuthorizedLinkAddons,
GetAuthorizedStorageAddons,
GetCitationAddons,
+ GetLinkAddons,
GetStorageAddons,
UpdateAuthorizedAddon,
} from '@shared/stores/addons';
@@ -53,101 +55,128 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddonsComponent {
- protected readonly tabOptions = ADDON_TAB_OPTIONS;
- protected readonly categoryOptions = ADDON_CATEGORY_OPTIONS;
-
- protected AddonTabValue = AddonTabValue;
- protected defaultTabValue = AddonTabValue.ALL_ADDONS;
-
- protected searchControl = new FormControl('');
-
- protected searchValue = signal('');
- protected selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES);
- protected selectedTab = signal(this.defaultTabValue);
-
- protected currentUser = select(UserSelectors.getCurrentUser);
- protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
- protected storageAddons = select(AddonsSelectors.getStorageAddons);
- protected citationAddons = select(AddonsSelectors.getCitationAddons);
- protected authorizedStorageAddons = select(AddonsSelectors.getAuthorizedStorageAddons);
- protected authorizedCitationAddons = select(AddonsSelectors.getAuthorizedCitationAddons);
-
- protected isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
- protected isUserReferenceLoading = select(AddonsSelectors.getAddonsUserReferenceLoading);
- protected isStorageAddonsLoading = select(AddonsSelectors.getStorageAddonsLoading);
- protected isCitationAddonsLoading = select(AddonsSelectors.getCitationAddonsLoading);
- protected isAuthorizedStorageAddonsLoading = select(AddonsSelectors.getAuthorizedStorageAddonsLoading);
- protected isAuthorizedCitationAddonsLoading = select(AddonsSelectors.getAuthorizedCitationAddonsLoading);
-
- protected isAddonsLoading = computed(() => {
+ readonly tabOptions = ADDON_TAB_OPTIONS;
+ readonly categoryOptions = ADDON_CATEGORY_OPTIONS;
+ readonly AddonTabValue = AddonTabValue;
+ readonly defaultTabValue = AddonTabValue.ALL_ADDONS;
+ searchControl = new FormControl('');
+ searchValue = signal('');
+ selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES);
+ selectedTab = signal(this.defaultTabValue);
+
+ currentUser = select(UserSelectors.getCurrentUser);
+ addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
+ storageAddons = select(AddonsSelectors.getStorageAddons);
+ citationAddons = select(AddonsSelectors.getCitationAddons);
+ linkAddons = select(AddonsSelectors.getLinkAddons);
+ authorizedStorageAddons = select(AddonsSelectors.getAuthorizedStorageAddons);
+ authorizedCitationAddons = select(AddonsSelectors.getAuthorizedCitationAddons);
+ authorizedLinkAddons = select(AddonsSelectors.getAuthorizedLinkAddons);
+
+ isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
+ isUserReferenceLoading = select(AddonsSelectors.getAddonsUserReferenceLoading);
+ isStorageAddonsLoading = select(AddonsSelectors.getStorageAddonsLoading);
+ isCitationAddonsLoading = select(AddonsSelectors.getCitationAddonsLoading);
+ isLinkAddonsLoading = select(AddonsSelectors.getLinkAddonsLoading);
+ isAuthorizedStorageAddonsLoading = select(AddonsSelectors.getAuthorizedStorageAddonsLoading);
+ isAuthorizedCitationAddonsLoading = select(AddonsSelectors.getAuthorizedCitationAddonsLoading);
+ isAuthorizedLinkAddonsLoading = select(AddonsSelectors.getAuthorizedLinkAddonsLoading);
+
+ isAddonsLoading = computed(() => {
return (
this.isStorageAddonsLoading() ||
this.isCitationAddonsLoading() ||
+ this.isLinkAddonsLoading() ||
this.isUserReferenceLoading() ||
this.isCurrentUserLoading()
);
});
- protected isAuthorizedAddonsLoading = computed(() => {
+ isAuthorizedAddonsLoading = computed(() => {
return (
this.isAuthorizedStorageAddonsLoading() ||
this.isAuthorizedCitationAddonsLoading() ||
+ this.isAuthorizedLinkAddonsLoading() ||
this.isUserReferenceLoading() ||
this.isCurrentUserLoading()
);
});
- protected actions = createDispatchMap({
+ actions = createDispatchMap({
getStorageAddons: GetStorageAddons,
getCitationAddons: GetCitationAddons,
+ getLinkAddons: GetLinkAddons,
getAuthorizedStorageAddons: GetAuthorizedStorageAddons,
getAuthorizedCitationAddons: GetAuthorizedCitationAddons,
+ getAuthorizedLinkAddons: GetAuthorizedLinkAddons,
createAuthorizedAddon: CreateAuthorizedAddon,
updateAuthorizedAddon: UpdateAuthorizedAddon,
getAddonsUserReference: GetAddonsUserReference,
deleteAuthorizedAddon: DeleteAuthorizedAddon,
});
- protected readonly allAuthorizedAddons = computed(() => {
- const authorizedAddons = [...this.authorizedStorageAddons(), ...this.authorizedCitationAddons()];
+ readonly allAuthorizedAddons = computed(() => {
+ const authorizedAddons = [
+ ...this.authorizedStorageAddons(),
+ ...this.authorizedCitationAddons(),
+ ...this.authorizedLinkAddons(),
+ ];
const searchValue = this.searchValue().toLowerCase();
- return authorizedAddons.filter((card) => card.displayName.includes(searchValue));
+ return authorizedAddons.filter((card) => card.displayName.toLowerCase().includes(searchValue));
});
- protected readonly userReferenceId = computed(() => {
+ readonly userReferenceId = computed(() => {
return this.addonsUserReference()[0]?.id;
});
- protected readonly currentAction = computed(() =>
- this.selectedCategory() === AddonCategory.EXTERNAL_STORAGE_SERVICES
- ? this.actions.getStorageAddons
- : this.actions.getCitationAddons
- );
+ readonly currentAction = computed(() => {
+ switch (this.selectedCategory()) {
+ case AddonCategory.EXTERNAL_STORAGE_SERVICES:
+ return this.actions.getStorageAddons;
+ case AddonCategory.EXTERNAL_CITATION_SERVICES:
+ return this.actions.getCitationAddons;
+ case AddonCategory.EXTERNAL_LINK_SERVICES:
+ return this.actions.getLinkAddons;
+ default:
+ return this.actions.getStorageAddons;
+ }
+ });
- protected readonly currentAddonsState = computed(() =>
- this.selectedCategory() === AddonCategory.EXTERNAL_STORAGE_SERVICES ? this.storageAddons() : this.citationAddons()
- );
+ readonly currentAddonsState = computed(() => {
+ switch (this.selectedCategory()) {
+ case AddonCategory.EXTERNAL_STORAGE_SERVICES:
+ return this.storageAddons();
+ case AddonCategory.EXTERNAL_CITATION_SERVICES:
+ return this.citationAddons();
+ case AddonCategory.EXTERNAL_LINK_SERVICES:
+ return this.linkAddons();
+ default:
+ return this.storageAddons();
+ }
+ });
- protected readonly filteredAddonCards = computed(() => {
+ readonly filteredAddonCards = computed(() => {
const searchValue = this.searchValue().toLowerCase();
- return this.currentAddonsState().filter((card) => card.externalServiceName.toLowerCase().includes(searchValue));
+ return this.currentAddonsState().filter(
+ (card) =>
+ card.externalServiceName.toLowerCase().includes(searchValue) ||
+ card.displayName.toLowerCase().includes(searchValue)
+ );
});
- protected onCategoryChange(value: Primitive): void {
+ onCategoryChange(value: Primitive): void {
if (typeof value === 'string') {
this.selectedCategory.set(value);
}
}
constructor() {
- // TODO There should not be three effects
effect(() => {
- if (this.currentUser()) {
+ if (this.currentUser() && !this.userReferenceId()) {
this.actions.getAddonsUserReference();
}
});
- // TODO There should not be three effects
effect(() => {
if (this.currentUser() && this.userReferenceId()) {
const action = this.currentAction();
@@ -156,12 +185,7 @@ export class AddonsComponent {
if (!addons?.length) {
action();
}
- }
- });
- // TODO There should not be three effects
- effect(() => {
- if (this.currentUser() && this.userReferenceId()) {
this.fetchAllAuthorizedAddons(this.userReferenceId());
}
});
@@ -174,5 +198,6 @@ export class AddonsComponent {
private fetchAllAuthorizedAddons(userReferenceId: string): void {
this.actions.getAuthorizedStorageAddons(userReferenceId);
this.actions.getAuthorizedCitationAddons(userReferenceId);
+ this.actions.getAuthorizedLinkAddons(userReferenceId);
}
}
diff --git a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.html b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.html
index 1fd573822..84b2feff5 100644
--- a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.html
+++ b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.html
@@ -18,9 +18,11 @@
{{ 'settings.addons.connectAddon.termsDescription' | translate }}
-
- {{ 'settings.addons.connectAddon.storageDescription' | translate }}
-
+ @if (addonTypeString() === AddonType.STORAGE) {
+
+ {{ 'settings.addons.connectAddon.storageDescription' | translate }}
+
+ }
diff --git a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts
index 8d60665c1..21ecad24b 100644
--- a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts
+++ b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts
@@ -11,10 +11,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { SubHeaderComponent } from '@osf/shared/components';
-import { ProjectAddonsStepperValue } from '@osf/shared/enums';
+import { AddonServiceNames, AddonType, ProjectAddonsStepperValue } from '@osf/shared/enums';
import { getAddonTypeString, isAuthorizedAddon } from '@osf/shared/helpers';
import { AddonSetupAccountFormComponent, AddonTermsComponent } from '@shared/components/addons';
import { AddonModel, AddonTerm, AuthorizedAccountModel, AuthorizedAddonRequestJsonApi } from '@shared/models';
+import { ToastService } from '@shared/services';
import { AddonsSelectors, CreateAuthorizedAddon, UpdateAuthorizedAddon } from '@shared/stores/addons';
@Component({
@@ -38,33 +39,35 @@ import { AddonsSelectors, CreateAuthorizedAddon, UpdateAuthorizedAddon } from '@
})
export class ConnectAddonComponent {
private readonly router = inject(Router);
+ private readonly toastService = inject(ToastService);
- protected readonly stepper = viewChild(Stepper);
- protected readonly ProjectAddonsStepperValue = ProjectAddonsStepperValue;
+ readonly stepper = viewChild(Stepper);
+ readonly AddonType = AddonType;
+ readonly ProjectAddonsStepperValue = ProjectAddonsStepperValue;
- protected terms = signal
([]);
- protected addon = signal(null);
- protected addonAuthUrl = signal('/settings/addons');
+ terms = signal([]);
+ addon = signal(null);
+ addonAuthUrl = signal('/settings/addons');
- protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
- protected createdAddon = select(AddonsSelectors.getCreatedOrUpdatedAuthorizedAddon);
- protected isCreatingAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedStorageAddonSubmitting);
- protected isAuthorized = computed(() => {
+ addonsUserReference = select(AddonsSelectors.getAddonsUserReference);
+ createdAddon = select(AddonsSelectors.getCreatedOrUpdatedAuthorizedAddon);
+ isCreatingAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedStorageAddonSubmitting);
+ isAuthorized = computed(() => {
return isAuthorizedAddon(this.addon());
});
- protected addonTypeString = computed(() => {
+ addonTypeString = computed(() => {
return getAddonTypeString(this.addon());
});
- protected actions = createDispatchMap({
+ actions = createDispatchMap({
createAuthorizedAddon: CreateAuthorizedAddon,
updateAuthorizedAddon: UpdateAuthorizedAddon,
});
- protected readonly userReferenceId = computed(() => {
+ readonly userReferenceId = computed(() => {
return this.addonsUserReference()[0]?.id;
});
- protected readonly baseUrl = computed(() => {
+ readonly baseUrl = computed(() => {
const currentUrl = this.router.url;
return currentUrl.split('/addons')[0];
});
@@ -92,10 +95,15 @@ export class ConnectAddonComponent {
).subscribe({
complete: () => {
const createdAddon = this.createdAddon();
- if (createdAddon) {
- this.addonAuthUrl.set(createdAddon.attributes.auth_url);
- window.open(createdAddon.attributes.auth_url, '_blank');
+ if (createdAddon?.authUrl) {
+ this.addonAuthUrl.set(createdAddon.authUrl);
+ window.open(createdAddon.authUrl, '_blank');
this.stepper()?.value.set(ProjectAddonsStepperValue.AUTH);
+ } else {
+ this.router.navigate([`${this.baseUrl()}/addons`]);
+ this.toastService.showSuccess('settings.addons.toast.createSuccess', {
+ addonName: AddonServiceNames[createdAddon?.externalServiceName as keyof typeof AddonServiceNames],
+ });
}
},
});
diff --git a/src/app/shared/components/addons/addon-card-list/addon-card-list.component.ts b/src/app/shared/components/addons/addon-card-list/addon-card-list.component.ts
index aaebeeaba..88922ed4a 100644
--- a/src/app/shared/components/addons/addon-card-list/addon-card-list.component.ts
+++ b/src/app/shared/components/addons/addon-card-list/addon-card-list.component.ts
@@ -3,7 +3,7 @@ import { TranslatePipe } from '@ngx-translate/core';
import { Component, input } from '@angular/core';
import { AddonCardComponent } from '@shared/components/addons';
-import { AddonModel, AuthorizedAccountModel, ConfiguredStorageAddonModel } from '@shared/models';
+import { AddonModel, AuthorizedAccountModel, ConfiguredAddonModel } from '@shared/models';
@Component({
selector: 'osf-addon-card-list',
@@ -12,7 +12,7 @@ import { AddonModel, AuthorizedAccountModel, ConfiguredStorageAddonModel } from
styleUrl: './addon-card-list.component.scss',
})
export class AddonCardListComponent {
- cards = input<(AddonModel | AuthorizedAccountModel | ConfiguredStorageAddonModel)[]>([]);
+ cards = input<(AddonModel | AuthorizedAccountModel | ConfiguredAddonModel)[]>([]);
cardButtonLabel = input('');
showDangerButton = input(false);
}
diff --git a/src/app/shared/components/addons/addon-card/addon-card.component.ts b/src/app/shared/components/addons/addon-card/addon-card.component.ts
index f2818d02e..941f0d666 100644
--- a/src/app/shared/components/addons/addon-card/addon-card.component.ts
+++ b/src/app/shared/components/addons/addon-card/addon-card.component.ts
@@ -9,7 +9,7 @@ import { Router } from '@angular/router';
import { getAddonTypeString, isConfiguredAddon } from '@osf/shared/helpers';
import { CustomConfirmationService, LoaderService } from '@osf/shared/services';
-import { AddonModel, AuthorizedAccountModel, ConfiguredStorageAddonModel } from '@shared/models';
+import { AddonModel, AuthorizedAccountModel, ConfiguredAddonModel } from '@shared/models';
import { DeleteAuthorizedAddon } from '@shared/stores/addons';
@Component({
@@ -24,7 +24,7 @@ export class AddonCardComponent {
private readonly loaderService = inject(LoaderService);
private readonly actions = createDispatchMap({ deleteAuthorizedAddon: DeleteAuthorizedAddon });
- readonly card = input(null);
+ readonly card = input(null);
readonly cardButtonLabel = input('');
readonly showDangerButton = input(false);
diff --git a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.ts b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.ts
index c0b2ea0a8..cbcb857eb 100644
--- a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.ts
+++ b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.ts
@@ -31,38 +31,38 @@ export class AddonSetupAccountFormComponent {
readonly formSubmit = output();
readonly backClick = output();
- protected readonly formControls = AddonFormControls;
+ readonly formControls = AddonFormControls;
get isFormValid() {
return this.addonForm().valid;
}
- protected readonly addonForm = computed>(() => {
+ readonly addonForm = computed>(() => {
return this.addonFormService.initializeForm(this.addon());
});
- protected readonly isAccessSecretKeysFormat = computed(() => {
+ readonly isAccessSecretKeysFormat = computed(() => {
return this.addon().credentialsFormat === CredentialsFormat.ACCESS_SECRET_KEYS;
});
- protected readonly isDataverseApiTokenFormat = computed(() => {
+ readonly isDataverseApiTokenFormat = computed(() => {
return this.addon().credentialsFormat === CredentialsFormat.DATAVERSE_API_TOKEN;
});
- protected readonly isUsernamePasswordFormat = computed(() => {
+ readonly isUsernamePasswordFormat = computed(() => {
return this.addon().credentialsFormat === CredentialsFormat.USERNAME_PASSWORD;
});
- protected readonly isRepoTokenFormat = computed(() => {
+ readonly isRepoTokenFormat = computed(() => {
return this.addon().credentialsFormat === CredentialsFormat.REPO_TOKEN;
});
- protected readonly isOAuthFormat = computed(() => {
+ readonly isOAuthFormat = computed(() => {
const format = this.addon().credentialsFormat;
return format === CredentialsFormat.OAUTH2 || format === CredentialsFormat.OAUTH;
});
- protected handleSubmit(): void {
+ handleSubmit(): void {
if (!this.isFormValid) return;
const formValue = this.addonForm().value;
@@ -76,7 +76,7 @@ export class AddonSetupAccountFormComponent {
this.formSubmit.emit(payload);
}
- protected handleBack(): void {
+ handleBack(): void {
this.backClick.emit();
}
}
diff --git a/src/app/shared/components/addons/folder-selector/folder-selector.component.ts b/src/app/shared/components/addons/folder-selector/folder-selector.component.ts
deleted file mode 100644
index 3aae2cca9..000000000
--- a/src/app/shared/components/addons/folder-selector/folder-selector.component.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { select } from '@ngxs/store';
-
-import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-
-import { MenuItem } from 'primeng/api';
-import { BreadcrumbModule } from 'primeng/breadcrumb';
-import { Button } from 'primeng/button';
-import { Card } from 'primeng/card';
-import { InputText } from 'primeng/inputtext';
-import { RadioButton } from 'primeng/radiobutton';
-import { Skeleton } from 'primeng/skeleton';
-
-import {
- ChangeDetectionStrategy,
- Component,
- computed,
- DestroyRef,
- effect,
- inject,
- input,
- model,
- OnInit,
- output,
- signal,
-} from '@angular/core';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
-
-import { OperationNames } from '@osf/features/project/addons/enums';
-import { OperationInvokeData, StorageItemModel } from '@osf/shared/models';
-import { AddonsSelectors } from '@osf/shared/stores';
-
-import { GoogleFilePickerComponent } from './google-file-picker/google-file-picker.component';
-
-@Component({
- selector: 'osf-folder-selector',
- templateUrl: './folder-selector.component.html',
- styleUrl: './folder-selector.component.scss',
- imports: [
- BreadcrumbModule,
- Button,
- Card,
- FormsModule,
- GoogleFilePickerComponent,
- InputText,
- RadioButton,
- ReactiveFormsModule,
- Skeleton,
- TranslatePipe,
- ],
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class FolderSelectorComponent implements OnInit {
- private destroyRef = inject(DestroyRef);
- private translateService = inject(TranslateService);
-
- isGoogleFilePicker = input.required();
- accountName = input.required();
- accountId = input.required();
- operationInvocationResult = input.required();
- accountNameControl = input(new FormControl());
- isCreateMode = input(false);
-
- selectedRootFolderId = model('/');
- operationInvoke = output();
- save = output();
- cancelSelection = output();
- readonly OperationNames = OperationNames;
- hasInputChanged = signal(false);
- hasFolderChanged = signal(false);
- public selectedRootFolder = signal(null);
- breadcrumbItems = signal