From ef64a9003a2d5dcd438c7e9236e26f5191faa1ad Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Jun 2025 10:21:36 +0300 Subject: [PATCH 1/6] fix(core): fixed core unit tests and styles --- .../breadcrumb/breadcrumb.component.spec.ts | 34 +++------ .../components/footer/footer.component.scss | 71 +++++++++---------- .../forbidden-page.component.spec.ts | 5 +- .../nav-menu/nav-menu.component.scss | 17 ++--- .../components/nav-menu/nav-menu.component.ts | 26 +++---- .../page-not-found.component.spec.ts | 5 +- .../request-access.component.spec.ts | 18 ++++- .../core/components/root/root.component.scss | 56 +++++++-------- .../components/topnav/topnav.component.scss | 21 +++--- src/app/core/models/user.models.ts | 1 - ...connect-configured-addon.component.spec.ts | 1 - 11 files changed, 125 insertions(+), 130 deletions(-) diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts b/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts index 591423245..edaaad4f9 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts +++ b/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts @@ -3,27 +3,32 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NavigationEnd, Router } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { BreadcrumbComponent } from './breadcrumb.component'; describe('BreadcrumbComponent', () => { let component: BreadcrumbComponent; let fixture: ComponentFixture; - let router: Router; const mockRouter = { url: '/test/path', events: of(new NavigationEnd(1, '/test/path', '/test/path')), }; + const mockActivatedRoute = { + snapshot: { + data: { skipBreadcrumbs: false }, + }, + firstChild: null, + }; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [BreadcrumbComponent], - providers: [MockProvider(Router, mockRouter)], + providers: [MockProvider(Router, mockRouter), { provide: ActivatedRoute, useValue: mockActivatedRoute }], }).compileComponents(); - router = TestBed.inject(Router); fixture = TestBed.createComponent(BreadcrumbComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -34,25 +39,4 @@ describe('BreadcrumbComponent', () => { expect(component['url']()).toBe('/test/path'); expect(component['parsedUrl']()).toEqual(['test', 'path']); }); - - it('should not show breadcrumb for home page', () => { - Object.defineProperty(router, 'url', { value: '/home' }); - component['url'].set('/home'); - fixture.detectChanges(); - - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('.breadcrumbs')).toBeNull(); - }); - - it('should show breadcrumb for valid path', () => { - Object.defineProperty(router, 'url', { value: '/settings/profile' }); - component['url'].set('/settings/profile'); - fixture.detectChanges(); - - const compiled = fixture.nativeElement as HTMLElement; - const breadcrumbs = compiled.querySelector('.breadcrumbs'); - expect(breadcrumbs).toBeTruthy(); - expect(breadcrumbs?.textContent).toContain('settings'); - expect(breadcrumbs?.textContent).toContain('profile'); - }); }); diff --git a/src/app/core/components/footer/footer.component.scss b/src/app/core/components/footer/footer.component.scss index c6b90a91a..882994d28 100644 --- a/src/app/core/components/footer/footer.component.scss +++ b/src/app/core/components/footer/footer.component.scss @@ -1,52 +1,47 @@ @use "assets/styles/mixins" as mix; @use "assets/styles/variables" as var; -:host { - display: block; - height: auto; +.footer-nav, +.footer-secondary-nav { + color: var.$dark-blue-1; + padding: 0 1.5rem; - .footer-nav, - .footer-secondary-nav { - color: var.$dark-blue-1; - padding: 0 1.5rem; - - .separator { - margin: 0 mix.rem(6px); - } + .separator { + margin: 0 mix.rem(6px); + } - a { - color: var.$dark-blue-1; - text-align: center; - } + a { + color: var.$dark-blue-1; + text-align: center; + } - .social-link { - background-color: var.$pr-blue-1; - border-radius: mix.rem(6px); - color: var.$white; - padding: mix.rem(6px); - width: mix.rem(36px); - height: mix.rem(36px); - - &:hover { - background-color: var.$pr-blue-3; - text-decoration: none; - } + .social-link { + background-color: var.$pr-blue-1; + border-radius: mix.rem(6px); + color: var.$white; + padding: mix.rem(6px); + width: mix.rem(36px); + height: mix.rem(36px); + + &:hover { + background-color: var.$pr-blue-3; + text-decoration: none; } } +} - .footer-links { - gap: mix.rem(6px); - } +.footer-links { + gap: mix.rem(6px); +} - .footer-nav { - background-color: var.$bg-blue-3; +.footer-nav { + background-color: var.$bg-blue-3; - .footer-socials { - gap: mix.rem(8px); - } + .footer-socials { + gap: mix.rem(8px); } +} - .footer-secondary-nav { - background: var.$bg-blue-2; - } +.footer-secondary-nav { + background: var.$bg-blue-2; } diff --git a/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts b/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts index 236e6821e..7980e7835 100644 --- a/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts +++ b/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts @@ -1,3 +1,6 @@ +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ForbiddenPageComponent } from './forbidden-page.component'; @@ -8,7 +11,7 @@ describe('ForbiddenPageComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ForbiddenPageComponent], + imports: [ForbiddenPageComponent, MockPipe(TranslatePipe)], }).compileComponents(); fixture = TestBed.createComponent(ForbiddenPageComponent); diff --git a/src/app/core/components/nav-menu/nav-menu.component.scss b/src/app/core/components/nav-menu/nav-menu.component.scss index 7228bf2ee..5f4e7a8aa 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.scss +++ b/src/app/core/components/nav-menu/nav-menu.component.scss @@ -1,15 +1,12 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; -:host { - .nav-menu { - width: 250px; - max-width: 250px; +.nav-menu { + width: 250px; + max-width: 250px; - .active { - background-color: var.$dark-blue-2; - border-radius: mix.rem(8px); - font-weight: 700; - } + .active { + background-color: var(--dark-blue-2); + border-radius: mix.rem(8px); + font-weight: 700; } } diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index fc1502642..035c14c66 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -9,7 +9,7 @@ import { Component, computed, inject, output } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; -import { NAV_ITEMS, PROJECT_MENU_ITEMS } from '@core/constants/nav-items.constant'; +import { NAV_ITEMS, PROJECT_MENU_ITEMS } from '@core/constants'; import { IconComponent } from '@osf/shared/components'; import { NavItem } from '@osf/shared/models'; @@ -20,29 +20,29 @@ import { NavItem } from '@osf/shared/models'; styleUrl: './nav-menu.component.scss', }) export class NavMenuComponent { - readonly #router = inject(Router); - readonly #route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); protected readonly navItems = NAV_ITEMS; protected readonly myProjectMenuItems = PROJECT_MENU_ITEMS; - protected readonly mainMenuItems = this.navItems.map((item) => this.#convertToMenuItem(item)); + protected readonly mainMenuItems = this.navItems.map((item) => this.convertToMenuItem(item)); closeMenu = output(); protected readonly currentRoute = toSignal( - this.#router.events.pipe( + this.router.events.pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd), - map(() => this.#getRouteInfo()) + map(() => this.getRouteInfo()) ), { - initialValue: this.#getRouteInfo(), + initialValue: this.getRouteInfo(), } ); protected readonly currentProjectId = computed(() => this.currentRoute().projectId); protected readonly isProjectRoute = computed(() => !!this.currentProjectId()); - #convertToMenuItem(item: NavItem): MenuItem { - const currentUrl = this.#router.url; + convertToMenuItem(item: NavItem): MenuItem { + const currentUrl = this.router.url; const isExpanded = item.isCollapsible && (currentUrl.startsWith(item.path) || @@ -53,13 +53,13 @@ export class NavMenuComponent { icon: item.icon ? `osf-icon-${item.icon}` : '', expanded: isExpanded, routerLink: item.isCollapsible ? undefined : item.path, - items: item.items?.map((subItem) => this.#convertToMenuItem(subItem)), + items: item.items?.map((subItem) => this.convertToMenuItem(subItem)), }; } - #getRouteInfo() { - const projectId = this.#route.firstChild?.snapshot.params['id'] || null; - const section = this.#route.firstChild?.firstChild?.snapshot.url[0]?.path || 'overview'; + getRouteInfo() { + const projectId = this.route.firstChild?.snapshot.params['id'] || null; + const section = this.route.firstChild?.firstChild?.snapshot.url[0]?.path || 'overview'; return { projectId, section }; } diff --git a/src/app/core/components/page-not-found/page-not-found.component.spec.ts b/src/app/core/components/page-not-found/page-not-found.component.spec.ts index 90873eba8..3da4b864d 100644 --- a/src/app/core/components/page-not-found/page-not-found.component.spec.ts +++ b/src/app/core/components/page-not-found/page-not-found.component.spec.ts @@ -1,3 +1,6 @@ +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PageNotFoundComponent } from './page-not-found.component'; @@ -8,7 +11,7 @@ describe('PageNotFoundComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PageNotFoundComponent], + imports: [PageNotFoundComponent, MockPipe(TranslatePipe)], }).compileComponents(); fixture = TestBed.createComponent(PageNotFoundComponent); diff --git a/src/app/core/components/request-access/request-access.component.spec.ts b/src/app/core/components/request-access/request-access.component.spec.ts index bc897e381..bf2e5594d 100644 --- a/src/app/core/components/request-access/request-access.component.spec.ts +++ b/src/app/core/components/request-access/request-access.component.spec.ts @@ -1,4 +1,14 @@ +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe, MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; + +import { ToastService } from '@osf/shared/services'; import { RequestAccessComponent } from './request-access.component'; @@ -8,7 +18,13 @@ describe('RequestAccessComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RequestAccessComponent], + imports: [RequestAccessComponent, MockPipe(TranslatePipe)], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + MockProvider(ToastService), + MockProvider(ActivatedRoute, { params: of({}) }), + ], }).compileComponents(); fixture = TestBed.createComponent(RequestAccessComponent); diff --git a/src/app/core/components/root/root.component.scss b/src/app/core/components/root/root.component.scss index 262425260..beee5455a 100644 --- a/src/app/core/components/root/root.component.scss +++ b/src/app/core/components/root/root.component.scss @@ -5,43 +5,43 @@ display: flex; height: 100vh; max-width: 100vw; +} + +.layout-desktop { + display: flex; + flex: 1; + background-color: var.$dark-blue-1; + max-width: 100vw; - .layout-desktop { + .content-wrapper { + position: relative; + background-color: var.$bg-blue-3; + border-radius: mix.rem(12px); + margin: mix.rem(6px); display: flex; + flex-direction: column; flex: 1; - background-color: var.$dark-blue-1; - max-width: 100vw; - - .content-wrapper { - position: relative; - background-color: var.$bg-blue-3; - border-radius: mix.rem(12px); - margin: mix.rem(6px); - display: flex; - flex-direction: column; - flex: 1; - overflow-y: auto; - } + overflow-y: auto; } +} + +.layout-tablet { + @include mix.flex-center; + flex: 1; + max-width: 100vw; - .layout-tablet { - @include mix.flex-center; + .content-wrapper { + @include mix.flex-column; + width: 100%; + height: 100%; flex: 1; - max-width: 100vw; + overflow-y: auto; + background-color: var.$bg-blue-3; - .content-wrapper { + .content { + position: relative; @include mix.flex-column; - width: 100%; - height: 100%; flex: 1; - overflow-y: auto; - background-color: var.$bg-blue-3; - - .content { - position: relative; - @include mix.flex-column; - flex: 1; - } } } } diff --git a/src/app/core/components/topnav/topnav.component.scss b/src/app/core/components/topnav/topnav.component.scss index 2deb687b0..595ed25eb 100644 --- a/src/app/core/components/topnav/topnav.component.scss +++ b/src/app/core/components/topnav/topnav.component.scss @@ -1,20 +1,19 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; :host { z-index: 1103; +} - .nav-container { - background: var.$dark-blue-1; - padding: mix.rem(28px) mix.rem(24px); - color: var.$white; +.nav-container { + background: var(--dark-blue-1); + padding: mix.rem(28px) mix.rem(24px); + color: var(--white); - .topnav-btn { - height: mix.rem(36px); - width: mix.rem(36px); + .topnav-btn { + height: mix.rem(36px); + width: mix.rem(36px); - --p-button-contrast-background: transparent; - --p-button-contrast-border-color: transparent; - } + --p-button-contrast-background: transparent; + --p-button-contrast-border-color: transparent; } } diff --git a/src/app/core/models/user.models.ts b/src/app/core/models/user.models.ts index eff1c718b..318400525 100644 --- a/src/app/core/models/user.models.ts +++ b/src/app/core/models/user.models.ts @@ -23,7 +23,6 @@ export interface UserSettings { subscribeOsfHelpEmail: boolean; } -// API Request/Response Models export interface UserGetResponse { id: string; type: string; diff --git a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts index d1a866264..88240f190 100644 --- a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts +++ b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts @@ -77,6 +77,5 @@ describe('ConnectAddonComponent', () => { expect(component).toBeTruthy(); expect(component['addon']()).toEqual(mockAddon); expect(component['terms']().length).toBeGreaterThan(0); - expect(component['addonForm']).toBeTruthy(); }); }); From 0d4e8ffe8960e0b75a79e58f21f2017e01431e12 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Jun 2025 10:27:28 +0300 Subject: [PATCH 2/6] fix(tokens): refactored tokens --- .../token-add-edit-form.component.html | 28 +++--- .../token-add-edit-form.component.scss | 11 --- .../token-add-edit-form.component.ts | 52 ++++++------ .../settings/tokens/models/scope.model.ts | 2 +- .../settings/tokens/models/tokens.model.ts | 4 - .../token-details.component.html | 29 ++++--- .../token-details.component.scss | 19 +---- .../token-details.component.spec.ts | 7 +- .../token-details/token-details.component.ts | 41 +++------ .../tokens-list/tokens-list.component.html | 17 ++-- .../tokens-list/tokens-list.component.scss | 43 ++-------- .../tokens-list/tokens-list.component.spec.ts | 15 +--- .../tokens-list/tokens-list.component.ts | 23 ++--- .../tokens/services/tokens.service.ts | 20 ++--- .../settings/tokens/store/tokens.models.ts | 8 +- .../settings/tokens/store/tokens.selectors.ts | 22 +++-- .../settings/tokens/store/tokens.state.ts | 85 ++++++++++++++----- .../settings/tokens/tokens.component.spec.ts | 11 +-- .../settings/tokens/tokens.component.ts | 37 ++++---- src/assets/styles/_variables.scss | 5 ++ src/assets/styles/overrides/card.scss | 20 ++--- src/assets/styles/overrides/checkbox.scss | 30 +------ 22 files changed, 224 insertions(+), 305 deletions(-) diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html index f6a2a10f3..62cca013f 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html @@ -1,15 +1,16 @@
- +
-

+

{{ 'settings.tokens.form.scopes.title' | translate }} -

-

+

+ +

{{ 'settings.tokens.form.scopes.description' | translate }}

@@ -18,10 +19,9 @@

@for (scope of tokenScopes(); track scope.id) {
+
- + {{ scope.attributes.description }}
@@ -31,12 +31,14 @@

{{ scope.id }}

@if (isEditMode()) { - +
+ +
} @else { (null); - protected readonly tokenId = toSignal(this.#route.params.pipe(map((params) => params['id']))); + + protected readonly tokenId = toSignal(this.route.params.pipe(map((params) => params['id']))); protected readonly dialogRef = inject(DynamicDialogRef); protected readonly TokenFormControls = TokenFormControls; - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly tokenScopes = this.#store.selectSignal(TokensSelectors.getScopes); + protected readonly tokenScopes = select(TokensSelectors.getScopes); readonly tokenForm: TokenForm = new FormGroup({ [TokenFormControls.TokenName]: new FormControl('', { @@ -55,7 +57,6 @@ export class TokenAddEditFormComponent implements OnInit { }); ngOnInit(): void { - this.#store.dispatch(GetTokens); if (this.initialValues()) { this.tokenForm.patchValue({ [TokenFormControls.TokenName]: this.initialValues()?.name, @@ -73,36 +74,31 @@ export class TokenAddEditFormComponent implements OnInit { } const { tokenName, scopes } = this.tokenForm.value; + if (!tokenName || !scopes) return; if (!this.isEditMode()) { - this.#store.dispatch(new CreateToken(tokenName, scopes)).subscribe({ + this.actions.createToken(tokenName, scopes).subscribe({ complete: () => { - const tokens = this.#store.selectSnapshot(TokensSelectors.getTokens); - const newToken = tokens[0]; + const tokens = select(TokensSelectors.getTokens); + const newToken = tokens()[0]; this.dialogRef.close(); - this.#showTokenCreatedDialog(newToken.name, newToken.tokenId); + this.showTokenCreatedDialog(newToken.name, newToken.tokenId); }, }); } else { - this.#store.dispatch(new UpdateToken(this.tokenId(), tokenName, scopes)).subscribe({ + this.actions.updateToken(this.tokenId(), tokenName, scopes).subscribe({ complete: () => { - this.#router.navigate(['settings/tokens']); + this.router.navigate(['settings/tokens']); }, }); } } - #showTokenCreatedDialog(tokenName: string, tokenValue: string) { - let dialogWidth = '500px'; - - if (this.isMobile()) { - dialogWidth = '345px'; - } - - this.#dialogService.open(TokenCreatedDialogComponent, { - width: dialogWidth, - header: this.#translateService.instant('settings.tokens.created-dialog.title'), + showTokenCreatedDialog(tokenName: string, tokenValue: string) { + this.dialogService.open(TokenCreatedDialogComponent, { + width: '500px', + header: this.translateService.instant('settings.tokens.createdDialog.title'), closeOnEscape: true, modal: true, closable: true, diff --git a/src/app/features/settings/tokens/models/scope.model.ts b/src/app/features/settings/tokens/models/scope.model.ts index 11abe44af..443b1a22f 100644 --- a/src/app/features/settings/tokens/models/scope.model.ts +++ b/src/app/features/settings/tokens/models/scope.model.ts @@ -1,4 +1,4 @@ -export interface Scope { +export interface ScopeJsonApi { id: string; type: string; attributes: { diff --git a/src/app/features/settings/tokens/models/tokens.model.ts b/src/app/features/settings/tokens/models/tokens.model.ts index 5fef6f9b4..d30f93337 100644 --- a/src/app/features/settings/tokens/models/tokens.model.ts +++ b/src/app/features/settings/tokens/models/tokens.model.ts @@ -1,4 +1,3 @@ -// API Request Model export interface TokenCreateRequestJsonApi { data: { attributes: { @@ -9,7 +8,6 @@ export interface TokenCreateRequestJsonApi { }; } -// API Response Model export interface TokenCreateResponseJsonApi { id: string; type: 'tokens'; @@ -21,7 +19,6 @@ export interface TokenCreateResponseJsonApi { }; } -// API Response Model for GET request export interface TokenGetResponseJsonApi { id: string; type: 'tokens'; @@ -32,7 +29,6 @@ export interface TokenGetResponseJsonApi { }; } -// Domain Models export interface Token { id: string; name: string; diff --git a/src/app/features/settings/tokens/pages/token-details/token-details.component.html b/src/app/features/settings/tokens/pages/token-details/token-details.component.html index dce71a1c5..64db4b575 100644 --- a/src/app/features/settings/tokens/pages/token-details/token-details.component.html +++ b/src/app/features/settings/tokens/pages/token-details/token-details.component.html @@ -1,23 +1,28 @@ -
- - @if (token()) { -
-

{{ token()?.name }}

+ @if (isLoading()) { + + } @else { + @if (token()) { +
+

{{ token()?.name }}

-
- +
+ +
-
- -

{{ 'settings.tokens.details.title' | translate }}

- -
+ +

{{ 'settings.tokens.details.title' | translate }}

+ +
+ } }
diff --git a/src/app/features/settings/tokens/pages/token-details/token-details.component.ts b/src/app/features/settings/tokens/pages/token-details/token-details.component.ts index db6ef490e..13a6a45a5 100644 --- a/src/app/features/settings/tokens/pages/token-details/token-details.component.ts +++ b/src/app/features/settings/tokens/pages/token-details/token-details.component.ts @@ -1,4 +1,4 @@ -import { createDispatchMap, Store } from '@ngxs/store'; +import { createDispatchMap, select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -7,10 +7,9 @@ import { Card } from 'primeng/card'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; -import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { IconComponent } from '@osf/shared/components'; +import { IconComponent, LoadingSpinnerComponent } from '@osf/shared/components'; import { CustomConfirmationService } from '@osf/shared/services'; import { TokenAddEditFormComponent } from '../../components'; @@ -18,7 +17,7 @@ import { DeleteToken, GetTokenById, TokensSelectors } from '../../store'; @Component({ selector: 'osf-token-details', - imports: [Button, Card, FormsModule, RouterLink, TokenAddEditFormComponent, TranslatePipe, IconComponent], + imports: [Button, Card, RouterLink, TranslatePipe, TokenAddEditFormComponent, IconComponent, LoadingSpinnerComponent], providers: [DialogService, DynamicDialogRef], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './token-details.component.html', @@ -34,6 +33,7 @@ export class TokenDetailsComponent implements OnInit { tokenId = signal(this.route.snapshot.paramMap.get('id') ?? ''); token = computed(() => this.store.selectSignal(TokensSelectors.getTokenById)()(this.tokenId())); + isLoading = select(TokensSelectors.isTokensLoading); ngOnInit(): void { if (this.tokenId()) { From c4aa4f3469f7ac3afdf28b4fcbbbab27399d19cf Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Jun 2025 11:36:13 +0300 Subject: [PATCH 4/6] fix(tokens): moved token state to feature token --- src/app/core/constants/ngxs-states.constant.ts | 2 -- src/app/features/settings/tokens/tokens.route.ts | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 7f622dcdd..93fb217f0 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -13,12 +13,10 @@ import { AccountSettingsState } from '@osf/features/settings/account-settings/st import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store'; import { ProfileSettingsState } from '@osf/features/settings/profile-settings/store/profile-settings.state'; -import { TokensState } from '@osf/features/settings/tokens/store'; import { AddonsState } from '@shared/stores/addons'; export const STATES = [ AuthState, - TokensState, AddonsState, UserState, MyProjectsState, diff --git a/src/app/features/settings/tokens/tokens.route.ts b/src/app/features/settings/tokens/tokens.route.ts index 3b2948299..c7f8093ae 100644 --- a/src/app/features/settings/tokens/tokens.route.ts +++ b/src/app/features/settings/tokens/tokens.route.ts @@ -1,10 +1,14 @@ +import { provideStore } from '@ngxs/store'; + import { Route } from '@angular/router'; +import { TokensState } from './store'; import { TokensComponent } from './tokens.component'; export const tokensAppsRoute: Route = { path: 'tokens', component: TokensComponent, + providers: [provideStore([TokensState])], children: [ { path: '', From d5cf518bdf4896f6aed8d682015fa52e87301f96 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Jun 2025 13:53:59 +0300 Subject: [PATCH 5/6] fix(tokens): updated models --- .../token-add-edit-form.component.html | 2 +- .../token-add-edit-form.component.ts | 4 ++-- .../features/settings/tokens/mappers/index.ts | 1 + .../settings/tokens/mappers/scope.mapper.ts | 10 +++++++++ .../settings/tokens/mappers/token.mapper.ts | 6 ++--- .../features/settings/tokens/models/index.ts | 4 +++- .../tokens/models/scope-json-api.model.ts | 10 +++++++++ .../settings/tokens/models/scope.model.ts | 10 ++------- ...okens.model.ts => token-json-api.model.ts} | 8 ------- .../settings/tokens/models/token.model.ts | 7 ++++++ .../token-details.component.spec.ts | 4 ++-- .../tokens-list/tokens-list.component.spec.ts | 4 ++-- .../tokens-list/tokens-list.component.ts | 4 ++-- .../tokens/services/tokens.service.ts | 22 ++++++++----------- .../settings/tokens/store/tokens.models.ts | 6 ++--- .../settings/tokens/store/tokens.selectors.ts | 6 ++--- .../settings/tokens/store/tokens.state.ts | 8 +++---- 17 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 src/app/features/settings/tokens/mappers/scope.mapper.ts create mode 100644 src/app/features/settings/tokens/models/scope-json-api.model.ts rename src/app/features/settings/tokens/models/{tokens.model.ts => token-json-api.model.ts} (80%) create mode 100644 src/app/features/settings/tokens/models/token.model.ts diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html index 62cca013f..d2f5260b0 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html @@ -22,7 +22,7 @@
- {{ scope.attributes.description }} + {{ scope.description }}
} diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts index 2922e86f4..e65f3c4f5 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts @@ -15,7 +15,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Token, TokenForm, TokenFormControls } from '../../models'; +import { TokenForm, TokenFormControls, TokenModel } from '../../models'; import { CreateToken, GetTokens, TokensSelectors, UpdateToken } from '../../store'; import { TokenCreatedDialogComponent } from '../token-created-dialog/token-created-dialog.component'; @@ -38,7 +38,7 @@ export class TokenAddEditFormComponent implements OnInit { }); isEditMode = input(false); - initialValues = input(null); + initialValues = input(null); protected readonly tokenId = toSignal(this.route.params.pipe(map((params) => params['id']))); protected readonly dialogRef = inject(DynamicDialogRef); diff --git a/src/app/features/settings/tokens/mappers/index.ts b/src/app/features/settings/tokens/mappers/index.ts index b56bed078..3dfc4f08d 100644 --- a/src/app/features/settings/tokens/mappers/index.ts +++ b/src/app/features/settings/tokens/mappers/index.ts @@ -1 +1,2 @@ +export * from './scope.mapper'; export * from './token.mapper'; diff --git a/src/app/features/settings/tokens/mappers/scope.mapper.ts b/src/app/features/settings/tokens/mappers/scope.mapper.ts new file mode 100644 index 000000000..998650bb6 --- /dev/null +++ b/src/app/features/settings/tokens/mappers/scope.mapper.ts @@ -0,0 +1,10 @@ +import { ScopeJsonApi, ScopeModel } from '../models'; + +export class ScopeMapper { + static fromResponse(response: ScopeJsonApi[]): ScopeModel[] { + return response.map((scope) => ({ + id: scope.id, + description: scope.attributes.description, + })); + } +} diff --git a/src/app/features/settings/tokens/mappers/token.mapper.ts b/src/app/features/settings/tokens/mappers/token.mapper.ts index 45cc20602..b3f0ae3ff 100644 --- a/src/app/features/settings/tokens/mappers/token.mapper.ts +++ b/src/app/features/settings/tokens/mappers/token.mapper.ts @@ -1,4 +1,4 @@ -import { Token, TokenCreateRequestJsonApi, TokenCreateResponseJsonApi, TokenGetResponseJsonApi } from '../models'; +import { TokenCreateRequestJsonApi, TokenCreateResponseJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; export class TokenMapper { static toRequest(name: string, scopes: string[]): TokenCreateRequestJsonApi { @@ -13,7 +13,7 @@ export class TokenMapper { }; } - static fromCreateResponse(response: TokenCreateResponseJsonApi): Token { + static fromCreateResponse(response: TokenCreateResponseJsonApi): TokenModel { return { id: response.id, name: response.attributes.name, @@ -23,7 +23,7 @@ export class TokenMapper { }; } - static fromGetResponse(response: TokenGetResponseJsonApi): Token { + static fromGetResponse(response: TokenGetResponseJsonApi): TokenModel { return { id: response.id, name: response.attributes.name, diff --git a/src/app/features/settings/tokens/models/index.ts b/src/app/features/settings/tokens/models/index.ts index 0b3ab05e3..8bc250995 100644 --- a/src/app/features/settings/tokens/models/index.ts +++ b/src/app/features/settings/tokens/models/index.ts @@ -1,3 +1,5 @@ export * from './scope.model'; +export * from './scope-json-api.model'; +export * from './token.model'; export * from './token-form.model'; -export * from './tokens.model'; +export * from './token-json-api.model'; diff --git a/src/app/features/settings/tokens/models/scope-json-api.model.ts b/src/app/features/settings/tokens/models/scope-json-api.model.ts new file mode 100644 index 000000000..443b1a22f --- /dev/null +++ b/src/app/features/settings/tokens/models/scope-json-api.model.ts @@ -0,0 +1,10 @@ +export interface ScopeJsonApi { + id: string; + type: string; + attributes: { + description: string; + }; + links: { + self: string; + }; +} diff --git a/src/app/features/settings/tokens/models/scope.model.ts b/src/app/features/settings/tokens/models/scope.model.ts index 443b1a22f..d92bdfc01 100644 --- a/src/app/features/settings/tokens/models/scope.model.ts +++ b/src/app/features/settings/tokens/models/scope.model.ts @@ -1,10 +1,4 @@ -export interface ScopeJsonApi { +export interface ScopeModel { id: string; - type: string; - attributes: { - description: string; - }; - links: { - self: string; - }; + description: string; } diff --git a/src/app/features/settings/tokens/models/tokens.model.ts b/src/app/features/settings/tokens/models/token-json-api.model.ts similarity index 80% rename from src/app/features/settings/tokens/models/tokens.model.ts rename to src/app/features/settings/tokens/models/token-json-api.model.ts index d30f93337..657116c8b 100644 --- a/src/app/features/settings/tokens/models/tokens.model.ts +++ b/src/app/features/settings/tokens/models/token-json-api.model.ts @@ -28,11 +28,3 @@ export interface TokenGetResponseJsonApi { owner: string; }; } - -export interface Token { - id: string; - name: string; - tokenId: string; - scopes: string[]; - ownerId: string; -} diff --git a/src/app/features/settings/tokens/models/token.model.ts b/src/app/features/settings/tokens/models/token.model.ts new file mode 100644 index 000000000..f4b5a3748 --- /dev/null +++ b/src/app/features/settings/tokens/models/token.model.ts @@ -0,0 +1,7 @@ +export interface TokenModel { + id: string; + name: string; + tokenId: string; + scopes: string[]; + ownerId: string; +} diff --git a/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts b/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts index 26e674e51..c256dcf3f 100644 --- a/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts +++ b/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts @@ -10,7 +10,7 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, provideRouter, RouterModule } from '@angular/router'; -import { Token } from '../../models'; +import { TokenModel } from '../../models'; import { TokenDetailsComponent } from './token-details.component'; @@ -20,7 +20,7 @@ describe('TokenDetailsComponent', () => { let store: Partial; let confirmationService: Partial; - const mockToken: Token = { + const mockToken: TokenModel = { id: '1', name: 'Test Token', tokenId: 'token1', diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts index 5c88308ac..b29edceeb 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts @@ -12,7 +12,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; -import { Token } from '../../models'; +import { TokenModel } from '../../models'; import { DeleteToken } from '../../store'; import { TokensListComponent } from './tokens-list.component'; @@ -23,7 +23,7 @@ describe('TokensListComponent', () => { let store: Partial; let confirmationService: Partial; - const mockTokens: Token[] = [ + const mockTokens: TokenModel[] = [ { id: '1', name: 'Test Token 1', diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts index 7c25bce69..11bf99ada 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts @@ -11,7 +11,7 @@ import { RouterLink } from '@angular/router'; import { CustomConfirmationService } from '@osf/shared/services'; -import { Token } from '../../models'; +import { TokenModel } from '../../models'; import { DeleteToken, GetTokens, TokensSelectors } from '../../store'; @Component({ @@ -33,7 +33,7 @@ export class TokensListComponent implements OnInit { this.actions.getTokens(); } - deleteToken(token: Token) { + deleteToken(token: TokenModel) { this.customConfirmationService.confirmDelete({ headerKey: 'settings.tokens.confirmation.delete.title', headerParams: { name: token.name }, diff --git a/src/app/features/settings/tokens/services/tokens.service.ts b/src/app/features/settings/tokens/services/tokens.service.ts index fb1b925d4..d7c512316 100644 --- a/src/app/features/settings/tokens/services/tokens.service.ts +++ b/src/app/features/settings/tokens/services/tokens.service.ts @@ -6,8 +6,8 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { TokenMapper } from '../mappers'; -import { ScopeJsonApi, Token, TokenCreateResponseJsonApi, TokenGetResponseJsonApi } from '../models'; +import { ScopeMapper, TokenMapper } from '../mappers'; +import { ScopeJsonApi, ScopeModel, TokenCreateResponseJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; import { environment } from 'src/environments/environment'; @@ -17,29 +17,25 @@ import { environment } from 'src/environments/environment'; export class TokensService { private readonly jsonApiService = inject(JsonApiService); - getScopes(): Observable { + getScopes(): Observable { return this.jsonApiService .get>(environment.apiUrl + '/scopes/') - .pipe(map((responses) => responses.data)); + .pipe(map((responses) => ScopeMapper.fromResponse(responses.data))); } - getTokens(): Observable { + getTokens(): Observable { return this.jsonApiService .get>(environment.apiUrl + '/tokens/') - .pipe( - map((responses) => { - return responses.data.map((response) => TokenMapper.fromGetResponse(response)); - }) - ); + .pipe(map((responses) => responses.data.map((response) => TokenMapper.fromGetResponse(response)))); } - getTokenById(tokenId: string): Observable { + getTokenById(tokenId: string): Observable { return this.jsonApiService .get>(`${environment.apiUrl}/tokens/${tokenId}/`) .pipe(map((response) => TokenMapper.fromGetResponse(response.data))); } - createToken(name: string, scopes: string[]): Observable { + createToken(name: string, scopes: string[]): Observable { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService @@ -47,7 +43,7 @@ export class TokensService { .pipe(map((response) => TokenMapper.fromCreateResponse(response))); } - updateToken(tokenId: string, name: string, scopes: string[]): Observable { + updateToken(tokenId: string, name: string, scopes: string[]): Observable { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService diff --git a/src/app/features/settings/tokens/store/tokens.models.ts b/src/app/features/settings/tokens/store/tokens.models.ts index 442e0190a..3f5a6a8b5 100644 --- a/src/app/features/settings/tokens/store/tokens.models.ts +++ b/src/app/features/settings/tokens/store/tokens.models.ts @@ -1,8 +1,8 @@ import { AsyncStateModel } from '@osf/shared/models'; -import { ScopeJsonApi, Token } from '../models'; +import { ScopeModel, TokenModel } from '../models'; export interface TokensStateModel { - scopes: AsyncStateModel; - tokens: AsyncStateModel; + scopes: AsyncStateModel; + tokens: AsyncStateModel; } diff --git a/src/app/features/settings/tokens/store/tokens.selectors.ts b/src/app/features/settings/tokens/store/tokens.selectors.ts index 299579950..5faf7c033 100644 --- a/src/app/features/settings/tokens/store/tokens.selectors.ts +++ b/src/app/features/settings/tokens/store/tokens.selectors.ts @@ -1,13 +1,13 @@ import { Selector } from '@ngxs/store'; -import { ScopeJsonApi, Token } from '../models'; +import { ScopeModel, TokenModel } from '../models'; import { TokensStateModel } from './tokens.models'; import { TokensState } from './tokens.state'; export class TokensSelectors { @Selector([TokensState]) - static getScopes(state: TokensStateModel): ScopeJsonApi[] { + static getScopes(state: TokensStateModel): ScopeModel[] { return state.scopes.data; } @@ -17,7 +17,7 @@ export class TokensSelectors { } @Selector([TokensState]) - static getTokens(state: TokensStateModel): Token[] { + static getTokens(state: TokensStateModel): TokenModel[] { return state.tokens.data; } diff --git a/src/app/features/settings/tokens/store/tokens.state.ts b/src/app/features/settings/tokens/store/tokens.state.ts index 6bff9199f..b760547cd 100644 --- a/src/app/features/settings/tokens/store/tokens.state.ts +++ b/src/app/features/settings/tokens/store/tokens.state.ts @@ -4,7 +4,7 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { Token } from '../models'; +import { TokenModel } from '../models'; import { TokensService } from '../services'; import { CreateToken, DeleteToken, GetScopes, GetTokenById, GetTokens, UpdateToken } from './tokens.actions'; @@ -59,7 +59,7 @@ export class TokensState { @Action(GetTokenById) getTokenById(ctx: StateContext, action: GetTokenById) { const state = ctx.getState(); - const tokenFromState = state.tokens.data.find((token: Token) => token.id === action.tokenId); + const tokenFromState = state.tokens.data.find((token: TokenModel) => token.id === action.tokenId); ctx.patchState({ tokens: { ...state.tokens, isLoading: true, error: null } }); @@ -84,7 +84,7 @@ export class TokensState { return this.tokensService.updateToken(action.tokenId, action.name, action.scopes).pipe( tap((updatedToken) => { const state = ctx.getState(); - const updatedTokens = state.tokens.data.map((token: Token) => + const updatedTokens = state.tokens.data.map((token: TokenModel) => token.id === action.tokenId ? updatedToken : token ); ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); @@ -101,7 +101,7 @@ export class TokensState { return this.tokensService.deleteToken(action.tokenId).pipe( tap(() => { const state = ctx.getState(); - const updatedTokens = state.tokens.data.filter((token: Token) => token.id !== action.tokenId); + const updatedTokens = state.tokens.data.filter((token: TokenModel) => token.id !== action.tokenId); ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); }), catchError((error) => this.handleError(ctx, 'tokens', error)) From d16cea364b695b1328ea68ad73c14bdb0e937e9c Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Jun 2025 17:59:22 +0300 Subject: [PATCH 6/6] fix(tokens): added toast messages --- .../create-view-link-dialog.component.html | 1 - src/app/features/search/search.component.scss | 3 ++ .../token-add-edit-form.component.html | 14 +++++-- .../token-add-edit-form.component.scss | 4 ++ .../token-add-edit-form.component.ts | 28 +++++++++++--- .../token-created-dialog.component.html | 22 +++++------ .../token-created-dialog.component.scss | 15 +++----- .../token-created-dialog.component.ts | 12 ++---- .../tokens/models/add-edit-token.model.ts | 4 ++ .../token-details/token-details.component.ts | 11 +++--- .../tokens-list/tokens-list.component.html | 38 +++++++++---------- .../tokens-list/tokens-list.component.ts | 8 +++- .../tokens/services/tokens.service.ts | 6 +-- .../settings/tokens/store/tokens.state.ts | 4 +- .../settings/tokens/tokens.component.ts | 2 +- .../features/settings/tokens/tokens.route.ts | 4 +- .../add-project-form.component.ts | 3 +- .../components/toast/toast.component.html | 2 +- .../components/toast/toast.component.scss | 4 ++ src/assets/i18n/en.json | 5 +++ src/assets/styles/overrides/input.scss | 2 +- 21 files changed, 111 insertions(+), 81 deletions(-) create mode 100644 src/app/features/settings/tokens/models/add-edit-token.model.ts diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html index 135c3e6b5..eb0f4aef4 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html @@ -55,7 +55,6 @@ class="w-full" styleClass="w-full" (click)="dialogRef.close()" - text severity="info" [label]="'project.contributors.addDialog.cancel' | translate" > diff --git a/src/app/features/search/search.component.scss b/src/app/features/search/search.component.scss index 3429bc57f..1ffdbbbe2 100644 --- a/src/app/features/search/search.component.scss +++ b/src/app/features/search/search.component.scss @@ -1,6 +1,9 @@ @use "assets/styles/variables" as var; :host { + display: flex; + flex-direction: column; + flex: 1; height: 100%; } diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html index d2f5260b0..bed186089 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html @@ -1,7 +1,12 @@
- - + +
@@ -15,7 +20,7 @@

-
+
@for (scope of tokenScopes(); track scope.id) {
@@ -50,7 +55,8 @@ class="w-12rem btn-full-width" [label]="'settings.tokens.form.buttons.create' | translate" type="submit" - [disabled]="!tokenForm.valid" + [disabled]="!tokenForm.valid || isLoading()" + [loading]="isLoading()" /> }
diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.scss b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.scss index e69de29bb..97c455de0 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.scss +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.scss @@ -0,0 +1,4 @@ +.scope-container { + height: 40vh; + overflow: auto; +} diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts index e65f3c4f5..32111c9e1 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts @@ -1,27 +1,29 @@ -import { createDispatchMap, select } from '@ngxs/store'; +import { createDispatchMap, select, Store } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { InputText } from 'primeng/inputtext'; import { map } from 'rxjs'; -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, input, OnInit } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { ToastService } from '@osf/shared/services'; + import { TokenForm, TokenFormControls, TokenModel } from '../../models'; import { CreateToken, GetTokens, TokensSelectors, UpdateToken } from '../../store'; import { TokenCreatedDialogComponent } from '../token-created-dialog/token-created-dialog.component'; @Component({ selector: 'osf-token-add-edit-form', - imports: [Button, InputText, ReactiveFormsModule, CommonModule, Checkbox, TranslatePipe], + imports: [Button, Checkbox, ReactiveFormsModule, TextInputComponent, TranslatePipe], templateUrl: './token-add-edit-form.component.html', styleUrl: './token-add-edit-form.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,6 +33,9 @@ export class TokenAddEditFormComponent implements OnInit { private readonly router = inject(Router); private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); + private readonly store = inject(Store); + private readonly actions = createDispatchMap({ createToken: CreateToken, getTokens: GetTokens, @@ -40,10 +45,13 @@ export class TokenAddEditFormComponent implements OnInit { isEditMode = input(false); initialValues = input(null); + inputLimits = InputLimits.fullName; + protected readonly tokenId = toSignal(this.route.params.pipe(map((params) => params['id']))); protected readonly dialogRef = inject(DynamicDialogRef); protected readonly TokenFormControls = TokenFormControls; protected readonly tokenScopes = select(TokensSelectors.getScopes); + protected readonly isLoading = select(TokensSelectors.isTokensLoading); readonly tokenForm: TokenForm = new FormGroup({ [TokenFormControls.TokenName]: new FormControl('', { @@ -56,6 +64,12 @@ export class TokenAddEditFormComponent implements OnInit { }), }); + constructor() { + effect(() => { + return this.isLoading() ? this.tokenForm.disable() : this.tokenForm.enable(); + }); + } + ngOnInit(): void { if (this.initialValues()) { this.tokenForm.patchValue({ @@ -80,7 +94,8 @@ export class TokenAddEditFormComponent implements OnInit { if (!this.isEditMode()) { this.actions.createToken(tokenName, scopes).subscribe({ complete: () => { - const tokens = select(TokensSelectors.getTokens); + this.toastService.showSuccess('settings.tokens.toastMessage.successCreate'); + const tokens = this.store.selectSignal(TokensSelectors.getTokens); const newToken = tokens()[0]; this.dialogRef.close(); this.showTokenCreatedDialog(newToken.name, newToken.tokenId); @@ -89,6 +104,7 @@ export class TokenAddEditFormComponent implements OnInit { } else { this.actions.updateToken(this.tokenId(), tokenName, scopes).subscribe({ complete: () => { + this.toastService.showSuccess('settings.tokens.toastMessage.successEdit'); this.router.navigate(['settings/tokens']); }, }); diff --git a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.html b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.html index 593c88b47..442daea1b 100644 --- a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.html +++ b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.html @@ -8,22 +8,18 @@
-
- - {{ 'settings.tokens.createdDialog.copyNotification' | translate }} - - - - - - - -
+ + + + + +
-

{{ 'settings.tokens.createdDialog.tokenWarning' | translate }}

+ +

{{ 'settings.tokens.createdDialog.tokenWarning' | translate }}

-
+
>('tokenInput'); readonly tokenName = input(this.config.data?.tokenName ?? ''); readonly tokenId = input(this.config.data?.tokenValue ?? ''); - protected readonly tokenCopiedNotificationVisible = signal(false); constructor() { afterNextRender(() => { @@ -42,9 +41,4 @@ export class TokenCreatedDialogComponent { } }); } - - protected tokenCopiedToClipboard(): void { - this.tokenCopiedNotificationVisible.set(true); - setTimeout(() => this.tokenCopiedNotificationVisible.set(false), 2000); - } } diff --git a/src/app/features/settings/tokens/models/add-edit-token.model.ts b/src/app/features/settings/tokens/models/add-edit-token.model.ts new file mode 100644 index 000000000..f7cfdac4e --- /dev/null +++ b/src/app/features/settings/tokens/models/add-edit-token.model.ts @@ -0,0 +1,4 @@ +export interface AddEditTokenModel { + name: string; + scopes: string[]; +} diff --git a/src/app/features/settings/tokens/pages/token-details/token-details.component.ts b/src/app/features/settings/tokens/pages/token-details/token-details.component.ts index 13a6a45a5..454885e29 100644 --- a/src/app/features/settings/tokens/pages/token-details/token-details.component.ts +++ b/src/app/features/settings/tokens/pages/token-details/token-details.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { IconComponent, LoadingSpinnerComponent } from '@osf/shared/components'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { TokenAddEditFormComponent } from '../../components'; import { DeleteToken, GetTokenById, TokensSelectors } from '../../store'; @@ -18,16 +18,17 @@ import { DeleteToken, GetTokenById, TokensSelectors } from '../../store'; @Component({ selector: 'osf-token-details', imports: [Button, Card, RouterLink, TranslatePipe, TokenAddEditFormComponent, IconComponent, LoadingSpinnerComponent], - providers: [DialogService, DynamicDialogRef], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './token-details.component.html', styleUrls: ['./token-details.component.scss'], + providers: [DialogService, DynamicDialogRef], }) export class TokenDetailsComponent implements OnInit { private readonly customConfirmationService = inject(CustomConfirmationService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly store = inject(Store); + private readonly toastService = inject(ToastService); private readonly actions = createDispatchMap({ getTokenById: GetTokenById, deleteToken: DeleteToken }); @@ -46,13 +47,13 @@ export class TokenDetailsComponent implements OnInit { headerKey: 'settings.tokens.confirmation.delete.title', headerParams: { name: this.token()?.name }, messageKey: 'settings.tokens.confirmation.delete.message', - onConfirm: () => { + onConfirm: () => this.actions.deleteToken(this.tokenId()).subscribe({ next: () => { + this.toastService.showSuccess('settings.tokens.toastMessage.successDelete'); this.router.navigate(['settings/tokens']); }, - }); - }, + }), }); } } diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.html b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.html index 7f73670ef..92fd4fdfa 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.html +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.html @@ -4,25 +4,6 @@

- @for (token of tokens(); track $index) { - -
- - {{ token.name }} - - -
- -
-
-
- } - @if (isLoading()) { @for (_ of [1, 2, 3]; track $index) { @@ -32,6 +13,25 @@
} + } @else { + @for (token of tokens(); track $index) { + +
+ + {{ token.name }} + + +
+ +
+
+
+ } } diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts index 11bf99ada..12b081d8d 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.ts @@ -9,7 +9,7 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { TokenModel } from '../../models'; import { DeleteToken, GetTokens, TokensSelectors } from '../../store'; @@ -24,6 +24,7 @@ import { DeleteToken, GetTokens, TokensSelectors } from '../../store'; export class TokensListComponent implements OnInit { private readonly actions = createDispatchMap({ getTokens: GetTokens, deleteToken: DeleteToken }); private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly toastService = inject(ToastService); protected readonly isLoading = select(TokensSelectors.isTokensLoading); @@ -38,7 +39,10 @@ export class TokensListComponent implements OnInit { headerKey: 'settings.tokens.confirmation.delete.title', headerParams: { name: token.name }, messageKey: 'settings.tokens.confirmation.delete.message', - onConfirm: () => this.actions.deleteToken(token.id), + onConfirm: () => + this.actions.deleteToken(token.id).subscribe({ + next: () => this.toastService.showSuccess('settings.tokens.toastMessage.successDelete'), + }), }); } } diff --git a/src/app/features/settings/tokens/services/tokens.service.ts b/src/app/features/settings/tokens/services/tokens.service.ts index d7c512316..b0e38c2ba 100644 --- a/src/app/features/settings/tokens/services/tokens.service.ts +++ b/src/app/features/settings/tokens/services/tokens.service.ts @@ -19,13 +19,13 @@ export class TokensService { getScopes(): Observable { return this.jsonApiService - .get>(environment.apiUrl + '/scopes/') + .get>(`${environment.apiUrl}/scopes/`) .pipe(map((responses) => ScopeMapper.fromResponse(responses.data))); } getTokens(): Observable { return this.jsonApiService - .get>(environment.apiUrl + '/tokens/') + .get>(`${environment.apiUrl}/tokens/`) .pipe(map((responses) => responses.data.map((response) => TokenMapper.fromGetResponse(response)))); } @@ -39,7 +39,7 @@ export class TokensService { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService - .post(environment.apiUrl + '/tokens/', request) + .post(`${environment.apiUrl}/tokens/`, request) .pipe(map((response) => TokenMapper.fromCreateResponse(response))); } diff --git a/src/app/features/settings/tokens/store/tokens.state.ts b/src/app/features/settings/tokens/store/tokens.state.ts index b760547cd..6fd6258d9 100644 --- a/src/app/features/settings/tokens/store/tokens.state.ts +++ b/src/app/features/settings/tokens/store/tokens.state.ts @@ -61,12 +61,12 @@ export class TokensState { const state = ctx.getState(); const tokenFromState = state.tokens.data.find((token: TokenModel) => token.id === action.tokenId); - ctx.patchState({ tokens: { ...state.tokens, isLoading: true, error: null } }); - if (tokenFromState) { return of(tokenFromState); } + ctx.patchState({ tokens: { ...state.tokens, isLoading: true, error: null } }); + return this.tokensService.getTokenById(action.tokenId).pipe( tap((token) => { const updatedTokens = [...state.tokens.data, token]; diff --git a/src/app/features/settings/tokens/tokens.component.ts b/src/app/features/settings/tokens/tokens.component.ts index 625db500a..f7addc852 100644 --- a/src/app/features/settings/tokens/tokens.component.ts +++ b/src/app/features/settings/tokens/tokens.component.ts @@ -41,7 +41,7 @@ export class TokensComponent implements OnInit { } createToken(): void { - const dialogWidth = this.isSmall() ? '95vw' : '800px'; + const dialogWidth = this.isSmall() ? '800px ' : '95vw'; this.dialogService.open(TokenAddEditFormComponent, { width: dialogWidth, diff --git a/src/app/features/settings/tokens/tokens.route.ts b/src/app/features/settings/tokens/tokens.route.ts index c7f8093ae..5e9b334df 100644 --- a/src/app/features/settings/tokens/tokens.route.ts +++ b/src/app/features/settings/tokens/tokens.route.ts @@ -1,4 +1,4 @@ -import { provideStore } from '@ngxs/store'; +import { provideStates } from '@ngxs/store'; import { Route } from '@angular/router'; @@ -8,7 +8,7 @@ import { TokensComponent } from './tokens.component'; export const tokensAppsRoute: Route = { path: 'tokens', component: TokensComponent, - providers: [provideStore([TokensState])], + providers: [provideStates([TokensState])], children: [ { path: '', diff --git a/src/app/shared/components/add-project-form/add-project-form.component.ts b/src/app/shared/components/add-project-form/add-project-form.component.ts index 431820943..6937e58bf 100644 --- a/src/app/shared/components/add-project-form/add-project-form.component.ts +++ b/src/app/shared/components/add-project-form/add-project-form.component.ts @@ -125,8 +125,7 @@ export class AddProjectFormComponent implements OnInit { this.#store.dispatch(new GetMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {})); this.dialogRef.close(); }, - error: (error) => { - console.error('Failed to create project:', error); + error: () => { this.isSubmitting.set(false); }, }); diff --git a/src/app/shared/components/toast/toast.component.html b/src/app/shared/components/toast/toast.component.html index 2637c390e..2edcc0abe 100644 --- a/src/app/shared/components/toast/toast.component.html +++ b/src/app/shared/components/toast/toast.component.html @@ -1,4 +1,4 @@ - +
{{ message.summary | translate: message.data?.translationParams }}
diff --git a/src/app/shared/components/toast/toast.component.scss b/src/app/shared/components/toast/toast.component.scss index e69de29bb..019a14ae8 100644 --- a/src/app/shared/components/toast/toast.component.scss +++ b/src/app/shared/components/toast/toast.component.scss @@ -0,0 +1,4 @@ +.toast-container { + position: fixed; + z-index: 2104; +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 92b9d0e20..b59ea1621 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -953,6 +953,11 @@ "copyNotification": "Copied!", "tokenWarning": "This is the only time your token will be displayed.", "closeButton": "Close" + }, + "toastMessage": { + "successDelete": "Token successfully deleted.", + "successCreate": "Token successfully created.", + "successEdit": "Token successfully updated." } }, "notifications": { diff --git a/src/assets/styles/overrides/input.scss b/src/assets/styles/overrides/input.scss index abcf73c70..33d934082 100644 --- a/src/assets/styles/overrides/input.scss +++ b/src/assets/styles/overrides/input.scss @@ -6,7 +6,7 @@ background: var.$white; border-color: var.$grey-2; - &:enabled:hover { + &:not([readonly]):enabled:hover { border-color: var.$pr-blue-1; } }