diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.ts b/src/app/core/components/breadcrumb/breadcrumb.component.ts index 06af64b2d..21ace7a94 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.ts +++ b/src/app/core/components/breadcrumb/breadcrumb.component.ts @@ -1,5 +1,6 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, computed, DestroyRef, inject, signal } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'osf-breadcrumb', @@ -9,8 +10,19 @@ import { Router } from '@angular/router'; }) export class BreadcrumbComponent { #router = inject(Router); + #destroyRef = inject(DestroyRef); protected readonly url = signal(this.#router.url); protected readonly parsedUrl = computed(() => { return this.url().split('/').filter(Boolean); }); + + constructor() { + this.#router.events + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe((event) => { + if (event instanceof NavigationEnd) { + this.url.set(this.#router.url); + } + }); + } } diff --git a/src/app/core/helpers/link-validator.helper.ts b/src/app/core/helpers/link-validator.helper.ts new file mode 100644 index 000000000..a7c406224 --- /dev/null +++ b/src/app/core/helpers/link-validator.helper.ts @@ -0,0 +1,16 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export function linkValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (!value) { + return null; + } + + const urlPattern = /^(https?):\/\/.+/i; + + const isValid = urlPattern.test(value); + + return isValid ? null : { link: true }; + }; +} diff --git a/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.html b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.html new file mode 100644 index 000000000..5aeaa6349 --- /dev/null +++ b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.html @@ -0,0 +1,58 @@ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
diff --git a/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.scss b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.spec.ts b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.spec.ts new file mode 100644 index 000000000..06cae3a5e --- /dev/null +++ b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateDeveloperAppComponent } from './create-developer-app.component'; + +describe('CreateDeveloperAppComponent', () => { + let component: CreateDeveloperAppComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CreateDeveloperAppComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CreateDeveloperAppComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.ts b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.ts new file mode 100644 index 000000000..063a2199d --- /dev/null +++ b/src/app/features/settings/developer-apps/create-developer-app/create-developer-app.component.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { Button } from 'primeng/button'; +import { InputText } from 'primeng/inputtext'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + DeveloperAppForm, + DeveloperAppFormFormControls, +} from '@osf/features/settings/developer-apps/developer-app.entities'; +import { linkValidator } from '@core/helpers/link-validator.helper'; + +@Component({ + selector: 'osf-create-developer-app', + imports: [Button, InputText, ReactiveFormsModule], + templateUrl: './create-developer-app.component.html', + styleUrl: './create-developer-app.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreateDeveloperAppComponent { + readonly dialogRef = inject(DynamicDialogRef); + protected readonly DeveloperAppFormFormControls = + DeveloperAppFormFormControls; + + readonly createAppForm: DeveloperAppForm = new FormGroup({ + [DeveloperAppFormFormControls.AppName]: new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }), + [DeveloperAppFormFormControls.ProjectHomePageUrl]: new FormControl('', { + nonNullable: true, + validators: [Validators.required, linkValidator()], + }), + [DeveloperAppFormFormControls.AppDescription]: new FormControl('', { + nonNullable: false, + }), + [DeveloperAppFormFormControls.AuthorizationCallbackUrl]: new FormControl( + '', + { + nonNullable: true, + validators: [Validators.required, linkValidator()], + }, + ), + }); + + submitForm(): void { + if (!this.createAppForm.valid) { + this.createAppForm.markAllAsTouched(); + this.createAppForm + .get([DeveloperAppFormFormControls.AppName]) + ?.markAsDirty(); + this.createAppForm + .get(DeveloperAppFormFormControls.ProjectHomePageUrl) + ?.markAsDirty(); + this.createAppForm + .get(DeveloperAppFormFormControls.AppDescription) + ?.markAsDirty(); + this.createAppForm + .get(DeveloperAppFormFormControls.AuthorizationCallbackUrl) + ?.markAsDirty(); + return; + } + + //TODO integrate API + this.dialogRef.close(); + } +} diff --git a/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.html b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.html new file mode 100644 index 000000000..f229fbbfc --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.html @@ -0,0 +1,175 @@ +
+ + +
+

{{ developerApp().appName + developerAppId() }}

+ +
+ +
+ +
+

Client ID

+ +

+ The client ID is the developer app's unique identifier and is safe to + share publicly. +

+ +
+ + Copied! + + + + + + + +
+
+
+ + +
+

Client Secret

+ +

+ The client secret is available only to you. Keep it private and do not + share it. +

+ +
+
+ + Copied! + + + + + + + +
+ + +
+ +
+ +
+
+
+ + +
+

Edit app

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
diff --git a/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.scss b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.scss new file mode 100644 index 000000000..6a9a327fb --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.scss @@ -0,0 +1,86 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +.content-container { + padding: 1.7rem; + color: var.$dark-blue-1; + background-color: var.$white; + + &.mobile { + padding: 1rem; + } + + .navigation-bar-container { + margin-bottom: 1.7rem; + } + + .tittle-container { + @include mix.flex-center-between; + margin-bottom: 3.4rem; + + &.mobile { + @include mix.flex-column; + align-items: inherit; + gap: 1.7rem; + } + } + + .cards-container { + @include mix.flex-column; + gap: 1.7rem; + + .card-body { + h2 { + margin-bottom: 1.7rem; + } + + p { + margin-bottom: 0.85rem; + } + + .client-secret-container { + @include mix.flex-align-center; + gap: 0.85rem; + margin-bottom: 1.7rem; + + &.mobile { + @include mix.flex-column; + align-items: start; + } + } + + .card-actions { + @include mix.flex-center-right; + } + + .copy-notification { + position: absolute; + top: -45px; + left: 50%; + transform: translateX(-50%); + padding: 10px; + opacity: 0; + transition: opacity 0.3s ease; + white-space: nowrap; + border-radius: 8px; + box-shadow: 0px 0px 4px 0px #00000029; + background: var.$white; + + &.visible { + opacity: 1; + } + + &:after { + content: ""; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + border-width: 5px 5px 0; + border-style: solid; + border-color: var.$grey-1 transparent transparent; + } + } + } + } +} diff --git a/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.spec.ts b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.spec.ts new file mode 100644 index 000000000..bebf34b45 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeveloperAppDetailsComponent } from './developer-app-details.component'; + +describe('DeveloperApplicationDetailsComponent', () => { + let component: DeveloperAppDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeveloperAppDetailsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DeveloperAppDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.ts b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.ts new file mode 100644 index 000000000..54884e8b3 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-app-details/developer-app-details.component.ts @@ -0,0 +1,189 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + inject, + OnInit, + signal, +} from '@angular/core'; +import { + DeveloperApp, + DeveloperAppFormFormControls, + DeveloperAppForm, +} from '@osf/features/settings/developer-apps/developer-app.entities'; +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { InputText } from 'primeng/inputtext'; +import { IconField } from 'primeng/iconfield'; +import { InputIcon } from 'primeng/inputicon'; +import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; +import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { linkValidator } from '@core/helpers/link-validator.helper'; +import { ConfirmationService } from 'primeng/api'; +import { defaultConfirmationConfig } from '@shared/helpers/default-confirmation-config.helper'; +import { timer } from 'rxjs'; + +@Component({ + selector: 'osf-developer-application-details', + imports: [ + Button, + Card, + RouterLink, + InputText, + IconField, + InputIcon, + CdkCopyToClipboard, + FormsModule, + ReactiveFormsModule, + ], + templateUrl: './developer-app-details.component.html', + styleUrl: './developer-app-details.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeveloperAppDetailsComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + private readonly activatedRoute = inject(ActivatedRoute); + private readonly confirmationService = inject(ConfirmationService); + private readonly isXSmall$ = inject(IS_XSMALL); + + isXSmall = toSignal(this.isXSmall$); + developerAppId = signal(null); + developerApp = signal({ + id: '1', + appName: 'Example name', + projHomePageUrl: 'https://example.com', + appDescription: 'Example description', + authorizationCallbackUrl: 'https://example.com/callback', + }); + isClientSecretVisible = signal(false); + clientSecret = signal( + 'clientsecretclientsecretclientsecretclientsecret', + ); + hiddenClientSecret = computed(() => + '*'.repeat(this.clientSecret().length), + ); + clientSecretCopiedNotificationVisible = signal(false); + + clientId = signal('clientid'); + clientIdCopiedNotificationVisible = signal(false); + + readonly DeveloperAppFormFormControls = DeveloperAppFormFormControls; + readonly editAppForm: DeveloperAppForm = new FormGroup({ + [DeveloperAppFormFormControls.AppName]: new FormControl( + this.developerApp().appName, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + [DeveloperAppFormFormControls.ProjectHomePageUrl]: new FormControl( + this.developerApp().projHomePageUrl, + { + nonNullable: true, + validators: [Validators.required, linkValidator()], + }, + ), + [DeveloperAppFormFormControls.AppDescription]: new FormControl( + this.developerApp().appDescription, + { + nonNullable: false, + }, + ), + [DeveloperAppFormFormControls.AuthorizationCallbackUrl]: new FormControl( + this.developerApp().authorizationCallbackUrl, + { + nonNullable: true, + validators: [Validators.required, linkValidator()], + }, + ), + }); + + ngOnInit(): void { + this.developerAppId.set(this.activatedRoute.snapshot.params['id']); + } + + deleteApp(): void { + this.confirmationService.confirm({ + ...defaultConfirmationConfig, + message: + "Are you sure you want to delete this developer app? All users' access tokens will be revoked. This cannot be reversed.", + header: `Delete App ${this.developerApp().appName}?`, + acceptButtonProps: { + ...defaultConfirmationConfig.acceptButtonProps, + severity: 'danger', + label: 'Delete', + }, + accept: () => { + //TODO integrate API + }, + }); + } + + resetClientSecret(): void { + this.confirmationService.confirm({ + ...defaultConfirmationConfig, + message: + 'Resetting the client secret will render your application unusable until it is updated with the new client secret,' + + ' and all users must reauthorize access. Previously issued access tokens will no longer work.' + + '

Are you sure you want to reset the client secret? This cannot be reversed.', + header: `Reset Client Secret?`, + acceptButtonProps: { + ...defaultConfirmationConfig.acceptButtonProps, + severity: 'danger', + label: 'Reset', + }, + accept: () => { + //TODO integrate API + }, + }); + } + + clientIdCopiedToClipboard(): void { + this.clientIdCopiedNotificationVisible.set(true); + + timer(2500) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.clientIdCopiedNotificationVisible.set(false); + }); + } + + clientSecretCopiedToClipboard(): void { + this.clientSecretCopiedNotificationVisible.set(true); + + timer(2500) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.clientSecretCopiedNotificationVisible.set(false); + }); + } + + submitForm(): void { + if (!this.editAppForm.valid) { + this.editAppForm.markAllAsTouched(); + this.editAppForm.get(DeveloperAppFormFormControls.AppName)?.markAsDirty(); + this.editAppForm + .get(DeveloperAppFormFormControls.ProjectHomePageUrl) + ?.markAsDirty(); + this.editAppForm + .get(DeveloperAppFormFormControls.AppDescription) + ?.markAsDirty(); + this.editAppForm + .get(DeveloperAppFormFormControls.AuthorizationCallbackUrl) + ?.markAsDirty(); + return; + } + + //TODO integrate API + } +} diff --git a/src/app/features/settings/developer-apps/developer-app.entities.ts b/src/app/features/settings/developer-apps/developer-app.entities.ts new file mode 100644 index 000000000..200a7ff5e --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-app.entities.ts @@ -0,0 +1,24 @@ +import { FormControl, FormGroup } from '@angular/forms'; +import { StringOrNull } from '@core/helpers/types.helper'; + +export interface DeveloperApp { + id: string; + appName: string; + projHomePageUrl: string; + appDescription: StringOrNull; + authorizationCallbackUrl: string; +} + +export enum DeveloperAppFormFormControls { + AppName = 'appName', + ProjectHomePageUrl = 'projHomePageUrl', + AppDescription = 'appDescription', + AuthorizationCallbackUrl = 'authorizationCallbackUrl', +} + +export type DeveloperAppForm = FormGroup<{ + [DeveloperAppFormFormControls.AppName]: FormControl; + [DeveloperAppFormFormControls.ProjectHomePageUrl]: FormControl; + [DeveloperAppFormFormControls.AppDescription]: FormControl; + [DeveloperAppFormFormControls.AuthorizationCallbackUrl]: FormControl; +}>; diff --git a/src/app/features/settings/developer-apps/developer-apps-container.component.html b/src/app/features/settings/developer-apps/developer-apps-container.component.html new file mode 100644 index 000000000..17a12df66 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-container.component.html @@ -0,0 +1,11 @@ + + +
+ +
diff --git a/src/app/features/settings/developer-apps/developer-apps-container.component.scss b/src/app/features/settings/developer-apps/developer-apps-container.component.scss new file mode 100644 index 000000000..3a0294d96 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-container.component.scss @@ -0,0 +1,12 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +:host { + @include mix.flex-column; + flex: 1; + + section { + @include mix.flex-column; + flex: 1; + } +} diff --git a/src/app/features/settings/developer-apps/developer-apps.component.spec.ts b/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts similarity index 52% rename from src/app/features/settings/developer-apps/developer-apps.component.spec.ts rename to src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts index 877f51a84..e481631a6 100644 --- a/src/app/features/settings/developer-apps/developer-apps.component.spec.ts +++ b/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DeveloperAppsComponent } from './developer-apps.component'; +import { DeveloperAppsContainerComponent } from './developer-apps-container.component'; describe('DeveloperAppsComponent', () => { - let component: DeveloperAppsComponent; - let fixture: ComponentFixture; + let component: DeveloperAppsContainerComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeveloperAppsComponent], + imports: [DeveloperAppsContainerComponent], }).compileComponents(); - fixture = TestBed.createComponent(DeveloperAppsComponent); + fixture = TestBed.createComponent(DeveloperAppsContainerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/settings/developer-apps/developer-apps-container.component.ts b/src/app/features/settings/developer-apps/developer-apps-container.component.ts new file mode 100644 index 000000000..fa654d3a8 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-container.component.ts @@ -0,0 +1,41 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; +import { DialogService } from 'primeng/dynamicdialog'; +import { CreateDeveloperAppComponent } from '@osf/features/settings/developer-apps/create-developer-app/create-developer-app.component'; +import { IS_MEDIUM, IS_XSMALL } from '@shared/utils/breakpoints.tokens'; +import { toSignal } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'osf-developer-apps', + imports: [RouterOutlet, SubHeaderComponent], + templateUrl: './developer-apps-container.component.html', + styleUrl: './developer-apps-container.component.scss', + providers: [DialogService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeveloperAppsContainerComponent { + private readonly dialogService = inject(DialogService); + isXSmall$ = inject(IS_XSMALL); + isMedium$ = inject(IS_MEDIUM); + isXSmall = toSignal(this.isXSmall$); + isMedium = toSignal(this.isMedium$); + + createDeveloperApp(): void { + let dialogWidth = '850px'; + if (this.isXSmall()) { + dialogWidth = '345px'; + } else if (this.isMedium()) { + dialogWidth = '500px'; + } + + this.dialogService.open(CreateDeveloperAppComponent, { + width: dialogWidth, + focusOnShow: false, + header: 'Create Developer App', + closeOnEscape: true, + modal: true, + closable: true, + }); + } +} diff --git a/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.html b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.html new file mode 100644 index 000000000..8c19106e4 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.html @@ -0,0 +1,26 @@ +
+

+ Third-party web applications can connect to the OSF on behalf of users via + the OAuth 2.0 web application flow. +

+ +
+ @for (developerApp of developerApplications(); track $index) { + +
+ +

{{ developerApp.appName }}

+
+
+ +
+
+
+ } +
+
diff --git a/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.scss b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.scss new file mode 100644 index 000000000..13e3d1ff1 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.scss @@ -0,0 +1,49 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +:host { + @include mix.flex-column; + flex: 1; + + .content-container { + flex: 1; + padding: 1.7rem; + color: var.$dark-blue-1; + background-color: var.$white; + + &.mobile { + padding: 1rem; + } + + p { + margin-bottom: 1.7rem; + } + + .applications-container { + @include mix.flex-column; + gap: 0.85rem; + + p-card { + .card-body { + &.mobile { + @include mix.flex-column; + gap: 0.85rem; + + a { + align-self: flex-start; + } + + .button-container { + align-self: flex-end; + width: 50%; + } + } + + &:not(.mobile) { + @include mix.flex-center-between; + } + } + } + } + } +} diff --git a/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.spec.ts b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.spec.ts new file mode 100644 index 000000000..11b7fe2fb --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeveloperAppsListComponent } from './developer-apps-list.component'; + +describe('DeveloperApplicationsListComponent', () => { + let component: DeveloperAppsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeveloperAppsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DeveloperAppsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.ts b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.ts new file mode 100644 index 000000000..cdf7ac554 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps-list/developer-apps-list.component.ts @@ -0,0 +1,61 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core'; +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { RouterLink } from '@angular/router'; +import { DeveloperApp } from '@osf/features/settings/developer-apps/developer-app.entities'; +import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { defaultConfirmationConfig } from '@shared/helpers/default-confirmation-config.helper'; +import { ConfirmationService } from 'primeng/api'; + +@Component({ + selector: 'osf-developer-applications-list', + imports: [Button, Card, RouterLink], + templateUrl: './developer-apps-list.component.html', + styleUrl: './developer-apps-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeveloperAppsListComponent { + private readonly confirmationService = inject(ConfirmationService); + #isXSmall$ = inject(IS_XSMALL); + isXSmall = toSignal(this.#isXSmall$); + + developerApplications = signal([ + { + id: '1', + appName: 'Developer app name example', + projHomePageUrl: 'https://example.com', + appDescription: 'Example description', + authorizationCallbackUrl: 'https://example.com/callback', + }, + { + id: '2', + appName: 'Developer app name example', + projHomePageUrl: 'https://example.com', + appDescription: 'Example description', + authorizationCallbackUrl: 'https://example.com/callback', + }, + ]); + + deleteApp(developerApp: DeveloperApp): void { + this.confirmationService.confirm({ + ...defaultConfirmationConfig, + message: + "Are you sure you want to delete this developer app? All users' access tokens will be revoked. This cannot be reversed.", + header: `Delete App ${developerApp.appName}?`, + acceptButtonProps: { + ...defaultConfirmationConfig.acceptButtonProps, + severity: 'danger', + label: 'Delete', + }, + accept: () => { + //TODO integrate API + }, + }); + } +} diff --git a/src/app/features/settings/developer-apps/developer-apps.component.html b/src/app/features/settings/developer-apps/developer-apps.component.html deleted file mode 100644 index 7ab621ed7..000000000 --- a/src/app/features/settings/developer-apps/developer-apps.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
- -

Developer apps

- Create Developer App -
-
-

- Third-party web applications can connect to the OSF on behalf of users via - the OAuth 2.0 web application flow. -

- -
- @for (developerApp of developerApplications; track $index) { - -
-

{{ developerApp }}

- Delete -
-
- } -
-
diff --git a/src/app/features/settings/developer-apps/developer-apps.component.scss b/src/app/features/settings/developer-apps/developer-apps.component.scss deleted file mode 100644 index a4448def7..000000000 --- a/src/app/features/settings/developer-apps/developer-apps.component.scss +++ /dev/null @@ -1,45 +0,0 @@ -@use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; - -:host { - @include mix.flex-column; - flex: 1; - - .header { - @include mix.flex-center-between; - width: 100%; - padding: 7.14rem 1.71rem 3.43rem 1.71rem; - background: var.$gradient-1; - - h1 { - margin-left: 0.85rem; - } - - p-button { - margin-left: auto; - } - - i { - color: var.$dark-blue-1; - font-size: 2.6rem; - } - } - - .content { - margin: 1.7rem; - color: var.$dark-blue-1; - - p { - margin-bottom: 1.7rem; - } - - .applications-container { - @include mix.flex-column; - gap: 0.85rem; - - .card-body { - @include mix.flex-center-between; - } - } - } -} diff --git a/src/app/features/settings/developer-apps/developer-apps.component.ts b/src/app/features/settings/developer-apps/developer-apps.component.ts deleted file mode 100644 index a9d59f78c..000000000 --- a/src/app/features/settings/developer-apps/developer-apps.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { Button } from 'primeng/button'; -import { Card } from 'primeng/card'; - -@Component({ - selector: 'osf-developer-apps', - imports: [Button, Card], - templateUrl: './developer-apps.component.html', - styleUrl: './developer-apps.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeveloperAppsComponent { - developerApplications: string[] = [ - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - 'Developer app name example', - ]; - - onDeleteDeveloperApp(developerApp: string): void { - console.log('delete', developerApp); - //TODO implement api integration - } -} diff --git a/src/app/features/settings/developer-apps/developer-apps.route.ts b/src/app/features/settings/developer-apps/developer-apps.route.ts new file mode 100644 index 000000000..0e3c87d52 --- /dev/null +++ b/src/app/features/settings/developer-apps/developer-apps.route.ts @@ -0,0 +1,23 @@ +import { Route } from '@angular/router'; +import { DeveloperAppsContainerComponent } from '@osf/features/settings/developer-apps/developer-apps-container.component'; + +export const developerAppsRoute: Route = { + path: 'developer-apps', + component: DeveloperAppsContainerComponent, + children: [ + { + path: '', + loadComponent: () => + import('./developer-apps-list/developer-apps-list.component').then( + (c) => c.DeveloperAppsListComponent, + ), + }, + { + path: ':id/details', + loadComponent: () => + import('./developer-app-details/developer-app-details.component').then( + (c) => c.DeveloperAppDetailsComponent, + ), + }, + ], +}; diff --git a/src/app/features/settings/settings.routes.ts b/src/app/features/settings/settings.routes.ts index 713b9a278..affc73cdc 100644 --- a/src/app/features/settings/settings.routes.ts +++ b/src/app/features/settings/settings.routes.ts @@ -1,5 +1,6 @@ import { Routes } from '@angular/router'; import { SettingsContainerComponent } from '@osf/features/settings/settings-container.component'; +import { developerAppsRoute } from '@osf/features/settings/developer-apps/developer-apps.route'; export const settingsRoutes: Routes = [ { @@ -20,13 +21,7 @@ export const settingsRoutes: Routes = [ (c) => c.AccountSettingsComponent, ), }, - { - path: 'developer-apps', - loadComponent: () => - import('./developer-apps/developer-apps.component').then( - (mod) => mod.DeveloperAppsComponent, - ), - }, + developerAppsRoute, { path: 'addons', children: [ diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss index dd31b7762..29f265659 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -148,6 +148,13 @@ } } +.btn-half-width { + width: 50%; + .p-button { + width: 100%; + } +} + .form-btn { .p-button { @include mix.flex-center; diff --git a/src/assets/styles/overrides/card.scss b/src/assets/styles/overrides/card.scss index c2b7bbd90..0066c7adf 100644 --- a/src/assets/styles/overrides/card.scss +++ b/src/assets/styles/overrides/card.scss @@ -17,3 +17,7 @@ } } } + +.mobile .p-card .p-card-body { + padding: 0.85rem; +} diff --git a/src/assets/styles/overrides/iconfield.scss b/src/assets/styles/overrides/iconfield.scss new file mode 100644 index 000000000..7781e8f4b --- /dev/null +++ b/src/assets/styles/overrides/iconfield.scss @@ -0,0 +1,14 @@ +@use "assets/styles/variables" as var; + +.p-iconfield { + display: inline-block; + + .p-inputicon { + cursor: pointer; + color: var.$dark-blue-1; + + i { + font-size: 1.2rem; + } + } +} diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 88b20489e..10e18939d 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -17,7 +17,7 @@ @use "./overrides/radio"; @use "./overrides/dropdown"; @use "./overrides/confirmation-dialog"; -@use "./overrides/tabs"; +@use "./overrides/iconfield"; @layer base, primeng, reset;