From 75937c2664f5f5391f6a014bdcde5209ca955185 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 16 Oct 2025 10:51:47 +0300 Subject: [PATCH 1/2] fix(my-projects): added new filter for projects --- .../pages/dashboard/dashboard.component.html | 8 +- .../dashboard/dashboard.component.spec.ts | 17 +- .../pages/dashboard/dashboard.component.ts | 4 +- .../constants/project-filter-options.const.ts | 17 ++ .../my-projects/my-projects.component.html | 35 +++- .../my-projects/my-projects.component.spec.ts | 176 +++++------------- .../my-projects/my-projects.component.ts | 27 ++- .../my-projects-table.component.html | 132 +++++++------ .../my-projects-table.component.spec.ts | 25 +-- .../my-projects-table.component.ts | 6 +- .../shared/enums/resource-search-mode.enum.ts | 6 +- .../shared/services/my-resources.service.ts | 8 + src/assets/i18n/en.json | 5 + 13 files changed, 223 insertions(+), 243 deletions(-) create mode 100644 src/app/features/my-projects/constants/project-filter-options.const.ts diff --git a/src/app/features/home/pages/dashboard/dashboard.component.html b/src/app/features/home/pages/dashboard/dashboard.component.html index d2e420a6d..059bf4be3 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.html +++ b/src/app/features/home/pages/dashboard/dashboard.component.html @@ -25,14 +25,18 @@ {{ 'home.loggedIn.dashboard.quickSearch.osf' | translate }}

+ + { +describe('DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; @@ -31,7 +37,7 @@ describe.skip('DashboardComponent', () => { imports: [ DashboardComponent, OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, LoadingSpinnerComponent), + ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, LoadingSpinnerComponent, SearchInputComponent), ], providers: [ { @@ -46,6 +52,7 @@ describe.skip('DashboardComponent', () => { dispatch: jest.fn(), }, }, + MockProviders(CustomDialogService, CustomConfirmationService, ProjectRedirectDialogService), ], }).compileComponents(); @@ -53,7 +60,7 @@ describe.skip('DashboardComponent', () => { component = fixture.componentInstance; }); - it('should show loading s pinner when projects are loading', () => { + it('should show loading spinner when projects are loading', () => { areProjectsLoadingSignal.set(true); fixture.detectChanges(); diff --git a/src/app/features/home/pages/dashboard/dashboard.component.ts b/src/app/features/home/pages/dashboard/dashboard.component.ts index 4d3ce1082..54aa5bba9 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.ts @@ -19,6 +19,7 @@ import { IconComponent, LoadingSpinnerComponent, MyProjectsTableComponent, + SearchInputComponent, SubHeaderComponent, } from '@osf/shared/components'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; @@ -34,6 +35,7 @@ import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shar Button, SubHeaderComponent, MyProjectsTableComponent, + SearchInputComponent, IconComponent, TranslatePipe, LoadingSpinnerComponent, @@ -68,8 +70,6 @@ export class DashboardComponent implements OnInit { readonly existsProjects = computed(() => this.projects().length || !!this.searchControl.value?.length); - emailAddress = ''; - constructor() { this.setupSearchSubscription(); this.setupTotalRecordsEffect(); diff --git a/src/app/features/my-projects/constants/project-filter-options.const.ts b/src/app/features/my-projects/constants/project-filter-options.const.ts new file mode 100644 index 000000000..8c3e2ea27 --- /dev/null +++ b/src/app/features/my-projects/constants/project-filter-options.const.ts @@ -0,0 +1,17 @@ +import { ResourceSearchMode } from '@osf/shared/enums'; +import { TabOption } from '@osf/shared/models'; + +export const PROJECT_FILTER_OPTIONS: TabOption[] = [ + { + value: ResourceSearchMode.User, + label: 'myProjects.tabOptions.all', + }, + { + value: ResourceSearchMode.Root, + label: 'myProjects.tabOptions.root', + }, + { + value: ResourceSearchMode.Component, + label: 'myProjects.tabOptions.component', + }, +]; diff --git a/src/app/features/my-projects/my-projects.component.html b/src/app/features/my-projects/my-projects.component.html index 543ed4e40..725a1ac05 100644 --- a/src/app/features/my-projects/my-projects.component.html +++ b/src/app/features/my-projects/my-projects.component.html @@ -28,10 +28,25 @@ } +
+ + + +
+ + + + + + + { let component: MyProjectsComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; let isMediumSubject: BehaviorSubject; - let queryParamsSubject: BehaviorSubject>; - let store: jest.Mocked; - let router: jest.Mocked; beforeEach(async () => { isMediumSubject = new BehaviorSubject(false); - queryParamsSubject = new BehaviorSubject>({}); - - queryParamsSubject.next({}); - - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if ( - selector === MyResourcesSelectors.getTotalProjects || - selector === MyResourcesSelectors.getTotalRegistrations || - selector === MyResourcesSelectors.getTotalPreprints || - selector === MyResourcesSelectors.getTotalBookmarks - ) - return () => 0; - if (selector === BookmarksSelectors.getBookmarksCollectionId) return () => null; - if ( - selector === MyResourcesSelectors.getProjects || - selector === MyResourcesSelectors.getRegistrations || - selector === MyResourcesSelectors.getPreprints || - selector === MyResourcesSelectors.getBookmarks - ) - return () => []; - return () => undefined; - }); + mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ tab: '1' }).build(); + mockRouter = RouterMockBuilder.create().build(); await TestBed.configureTestingModule({ imports: [ MyProjectsComponent, OSFTestingModule, - ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, SelectComponent), + ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, SelectComponent, SearchInputComponent), ], providers: [ - MockProvider(Store, MOCK_STORE), - MockProvider(DialogService, { open: jest.fn() }), - MockProvider(ConfirmationService, { confirm: jest.fn() }), - MockProvider(ActivatedRoute, { queryParams: queryParamsSubject.asObservable() }), - MockProvider(Router, { navigate: jest.fn() }), + provideMockStore({ + signals: [ + { selector: MyResourcesSelectors.getTotalProjects, value: 0 }, + { selector: MyResourcesSelectors.getTotalRegistrations, value: 0 }, + { selector: MyResourcesSelectors.getTotalPreprints, value: 0 }, + { selector: MyResourcesSelectors.getTotalBookmarks, value: 0 }, + { selector: BookmarksSelectors.getBookmarksCollectionId, value: null }, + { selector: MyResourcesSelectors.getProjects, value: [] }, + { selector: MyResourcesSelectors.getRegistrations, value: [] }, + { selector: MyResourcesSelectors.getPreprints, value: [] }, + { selector: MyResourcesSelectors.getBookmarks, value: [] }, + ], + }), + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: Router, useValue: mockRouter }, + MockProvider(CustomDialogService), MockProvider(IS_MEDIUM, isMediumSubject), - MockProvider(ProjectRedirectDialogService, { showProjectRedirectDialog: jest.fn() }), + MockProvider(ProjectRedirectDialogService), ], }).compileComponents(); fixture = TestBed.createComponent(MyProjectsComponent); component = fixture.componentInstance; - store = TestBed.inject(Store) as jest.Mocked; - router = TestBed.inject(Router) as jest.Mocked; - - store.dispatch.mockReturnValue(of(undefined)); - - (component as any).queryParams = () => ({}); - fixture.detectChanges(); }); @@ -88,110 +73,43 @@ describe('MyProjectsComponent', () => { expect(component).toBeTruthy(); }); - it('should update component state from query params', () => { - component.updateComponentState({ page: 2, size: 20, search: 'q', sortColumn: 'name', sortOrder: SortOrder.Desc }); - - expect(component.currentPage()).toBe(2); - expect(component.currentPageSize()).toBe(20); - expect(component.searchControl.value).toBe('q'); - expect(component.sortColumn()).toBe('name'); - expect(component.sortOrder()).toBe(SortOrder.Desc); - expect(component.tableParams().firstRowIndex).toBe(20); - expect(component.tableParams().rows).toBe(20); - }); - - it('should create filters depending on tab', () => { - const filtersProjects = component.createFilters({ - page: 1, - size: 10, - search: 's', - sortColumn: 'name', - sortOrder: SortOrder.Asc, - }); - expect(filtersProjects.searchValue).toBe('s'); - expect(filtersProjects.searchFields).toEqual(['title', 'tags', 'description']); - - component.selectedTab.set(MyProjectsTab.Preprints); - const filtersPreprints = component.createFilters({ - page: 2, - size: 25, - search: 's2', - sortColumn: 'date', - sortOrder: SortOrder.Desc, - }); - expect(filtersPreprints.searchFields).toEqual(['title', 'tags']); - }); - - it('should fetch data for projects tab and stop loading', () => { - jest.clearAllMocks(); - store.dispatch.mockReturnValue(of(undefined)); - - component.fetchDataForCurrentTab({ page: 1, size: 10, search: 's', sortColumn: 'name', sortOrder: SortOrder.Asc }); - - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetMyProjects)); + it('should fetch data for projects tab', () => { + expect(component.selectedTab()).toBe(MyProjectsTab.Projects); expect(component.isLoading()).toBe(false); }); - it('should handle search and update query params', () => { - jest.clearAllMocks(); - queryParamsSubject.next({ sortColumn: 'name', sortOrder: 'desc', size: '25' }); - - component.handleSearch('query'); - - expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: TestBed.inject(ActivatedRoute), - queryParams: { page: '1', search: 'query', tab: '1' }, - }); - }); - it('should paginate and update query params', () => { - jest.clearAllMocks(); - queryParamsSubject.next({ sortColumn: 'title', sortOrder: 'asc' }); - component.onPageChange({ first: 30, rows: 15 } as any); - expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: TestBed.inject(ActivatedRoute), + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: mockActivatedRoute, queryParams: { page: '3', size: '15', tab: '1' }, }); }); it('should sort and update query params', () => { - jest.clearAllMocks(); - component.onSort({ field: 'updated', order: SortOrder.Desc } as any); - expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: TestBed.inject(ActivatedRoute), + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: mockActivatedRoute, queryParams: { sortColumn: 'updated', sortOrder: 'desc', tab: '1' }, }); }); it('should clear and reset on tab change', () => { - jest.clearAllMocks(); - queryParamsSubject.next({ size: '50' }); - - component.onTabChange(1); + component.onTabChange(MyProjectsTab.Registrations); - expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: TestBed.inject(ActivatedRoute), - queryParams: { page: '1', tab: '1' }, + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: mockActivatedRoute, + queryParams: { page: '1', tab: '2' }, }); - - expect(store.dispatch).toHaveBeenCalled(); }); - it('should navigate to project and set active project', () => { + it('should navigate to project', () => { const project = { id: 'p1' } as any; component.navigateToProject(project); - expect(component.activeProject()).toEqual(project); - expect(router.navigate).toHaveBeenCalledWith(['p1']); - }); - it('should navigate to registry and set active project', () => { - const reg = { id: 'r1' } as any; - component.navigateToRegistry(reg); - expect(component.activeProject()).toEqual(reg); - expect(router.navigate).toHaveBeenCalledWith(['r1']); + expect(component.activeProject()).toEqual(project); + expect(mockRouter.navigate).toHaveBeenCalledWith(['p1']); }); }); diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 403912687..b9381dc6a 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -23,7 +23,12 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { MyProjectsTableComponent, SelectComponent, SubHeaderComponent } from '@osf/shared/components'; +import { + MyProjectsTableComponent, + SearchInputComponent, + SelectComponent, + SubHeaderComponent, +} from '@osf/shared/components'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; import { ResourceType, SortOrder } from '@osf/shared/enums'; import { IS_MEDIUM } from '@osf/shared/helpers'; @@ -40,6 +45,7 @@ import { } from '@osf/shared/stores'; import { CustomDialogService, ProjectRedirectDialogService } from '@shared/services'; +import { PROJECT_FILTER_OPTIONS } from './constants/project-filter-options.const'; import { MyProjectsQueryService } from './services/my-projects-query.service'; import { MyProjectsTableParamsService } from './services/my-projects-table-params.service'; import { CreateProjectDialogComponent } from './components'; @@ -49,16 +55,17 @@ import { MyProjectsTab } from './enums'; @Component({ selector: 'osf-my-projects', imports: [ - SubHeaderComponent, FormsModule, Tab, TabList, TabPanel, TabPanels, Tabs, + SubHeaderComponent, MyProjectsTableComponent, - TranslatePipe, + SearchInputComponent, SelectComponent, + TranslatePipe, ], templateUrl: './my-projects.component.html', styleUrl: './my-projects.component.scss', @@ -78,6 +85,8 @@ export class MyProjectsComponent implements OnInit { readonly isMedium = toSignal(inject(IS_MEDIUM)); readonly tabOptions = MY_PROJECTS_TABS; readonly tabOption = MyProjectsTab; + readonly projectFilterOption = PROJECT_FILTER_OPTIONS; + readonly selectedProjectFilterOption = signal(PROJECT_FILTER_OPTIONS[0].value); readonly searchControl = new FormControl(''); @@ -137,10 +146,20 @@ export class MyProjectsComponent implements OnInit { onTabChange(tabIndex: number): void { this.actions.clearMyProjects(); this.selectedTab.set(tabIndex); + this.selectedProjectFilterOption.set(PROJECT_FILTER_OPTIONS[0].value); const current = this.queryService.getRawParams(); this.queryService.handleTabSwitch(current, this.selectedTab()); } + onProjectFilterChange(): void { + const params = this.queryParams(); + + if (params) { + const queryParams = this.queryService.toQueryModel(params); + this.fetchDataForCurrentTab(queryParams); + } + } + createProject(): void { this.customDialogService .open(CreateProjectDialogComponent, { @@ -283,7 +302,7 @@ export class MyProjectsComponent implements OnInit { let action$; switch (this.selectedTab()) { case MyProjectsTab.Projects: - action$ = this.actions.getMyProjects(pageNumber, pageSize, filters); + action$ = this.actions.getMyProjects(pageNumber, pageSize, filters, this.selectedProjectFilterOption()); break; case MyProjectsTab.Registrations: action$ = this.actions.getMyRegistrations(pageNumber, pageSize, filters); diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index 7ea9b8def..d56a1cb68 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -1,70 +1,66 @@ -
- - - - - - - {{ 'myProjects.table.columns.title' | translate }} - - - {{ 'myProjects.table.columns.contributors' | translate }} - - {{ 'myProjects.table.columns.modified' | translate }} - - + + + + + {{ 'myProjects.table.columns.title' | translate }} + + + {{ 'myProjects.table.columns.contributors' | translate }} + + {{ 'myProjects.table.columns.modified' | translate }} + + + + + + @if (item?.id) { + + +

+ + {{ item.title }} +

+ + + @for (contributor of item.contributors; track contributor) { + {{ contributor.fullName }}{{ $last ? '' : ', ' }} + } + + {{ item.dateModified | date: 'MMM d, y, h:mm a' }} -
- - @if (item?.id) { - - -

- - {{ item.title }} -

- - - @for (contributor of item.contributors; track contributor) { - {{ contributor.fullName }}{{ $last ? '' : ', ' }} - } - - {{ item.dateModified | date: 'MMM d, y, h:mm a' }} - - } @else { - - - - - - } -
- - - - {{ 'common.search.noResultsFound' | translate }} + } @else { + + + + - -
-
+ } + + + + + {{ 'common.search.noResultsFound' | translate }} + + + diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts b/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts index e698feb51..0843ffe6a 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts @@ -1,13 +1,13 @@ import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormControl } from '@angular/forms'; import { SortOrder } from '@osf/shared/enums/sort-order.enum'; import { TableParameters } from '@osf/shared/models/table-parameters.model'; -import { SearchInputComponent } from '@shared/components'; import { MyResourcesItem } from '@shared/models/my-resources/my-resources.models'; +import { IconComponent } from '../icon/icon.component'; + import { MyProjectsTableComponent } from './my-projects-table.component'; import { MOCK_CONTRIBUTOR, TranslateServiceMock } from '@testing/mocks'; @@ -39,11 +39,9 @@ describe('MyProjectsTableComponent', () => { }, ]; - const mockSearchControl = new FormControl(''); - beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MyProjectsTableComponent, MockComponent(SearchInputComponent)], + imports: [MyProjectsTableComponent, MockComponent(IconComponent)], providers: [TranslateServiceMock], }).compileComponents(); @@ -52,11 +50,9 @@ describe('MyProjectsTableComponent', () => { fixture.componentRef.setInput('items', mockItems); fixture.componentRef.setInput('tableParams', mockTableParams); - fixture.componentRef.setInput('searchControl', mockSearchControl); fixture.componentRef.setInput('sortColumn', 'title'); fixture.componentRef.setInput('sortOrder', SortOrder.Asc); fixture.componentRef.setInput('isLoading', false); - fixture.componentRef.setInput('searchPlaceholder', 'myProjects.table.searchPlaceholder'); fixture.detectChanges(); }); @@ -73,10 +69,6 @@ describe('MyProjectsTableComponent', () => { expect(component.tableParams()).toEqual(mockTableParams); }); - it('should set searchControl input', () => { - expect(component.searchControl()).toBe(mockSearchControl); - }); - it('should set sortColumn input', () => { expect(component.sortColumn()).toBe('title'); }); @@ -89,17 +81,6 @@ describe('MyProjectsTableComponent', () => { expect(component.isLoading()).toBe(false); }); - it('should set searchPlaceholder input', () => { - expect(component.searchPlaceholder()).toBe('myProjects.table.searchPlaceholder'); - }); - - it('should render search input when not loading', () => { - const compiled = fixture.nativeElement; - const searchInput = compiled.querySelector('osf-search-input'); - - expect(searchInput).toBeTruthy(); - }); - it('should render table when not loading', () => { const compiled = fixture.nativeElement; const table = compiled.querySelector('p-table'); diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.ts b/src/app/shared/components/my-projects-table/my-projects-table.component.ts index 2413698b8..4bfaab38e 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.ts +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.ts @@ -6,17 +6,15 @@ import { TableModule, TablePageEvent } from 'primeng/table'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FormControl } from '@angular/forms'; import { SortOrder } from '@osf/shared/enums'; import { MyResourcesItem, TableParameters } from '@osf/shared/models'; import { IconComponent } from '../icon/icon.component'; -import { SearchInputComponent } from '../search-input/search-input.component'; @Component({ selector: 'osf-my-projects-table', - imports: [CommonModule, TableModule, SearchInputComponent, IconComponent, Skeleton, TranslatePipe], + imports: [CommonModule, TableModule, IconComponent, Skeleton, TranslatePipe], templateUrl: './my-projects-table.component.html', styleUrl: './my-projects-table.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -24,11 +22,9 @@ import { SearchInputComponent } from '../search-input/search-input.component'; export class MyProjectsTableComponent { items = input([]); tableParams = input.required(); - searchControl = input(new FormControl('')); sortColumn = input(undefined); sortOrder = input(SortOrder.Asc); isLoading = input(false); - searchPlaceholder = input('myProjects.table.searchPlaceholder'); pageChange = output(); sort = output(); diff --git a/src/app/shared/enums/resource-search-mode.enum.ts b/src/app/shared/enums/resource-search-mode.enum.ts index 74508a4f0..9d758a251 100644 --- a/src/app/shared/enums/resource-search-mode.enum.ts +++ b/src/app/shared/enums/resource-search-mode.enum.ts @@ -1,4 +1,6 @@ export enum ResourceSearchMode { - User = 'user', - All = 'all', + All = 1, + Root, + Component, + User, } diff --git a/src/app/shared/services/my-resources.service.ts b/src/app/shared/services/my-resources.service.ts index 6599bc8b8..60c9d6723 100644 --- a/src/app/shared/services/my-resources.service.ts +++ b/src/app/shared/services/my-resources.service.ts @@ -99,6 +99,14 @@ export class MyResourcesService { url = endpoint.startsWith('collections/') ? `${this.apiUrl}/${endpoint}` : `${this.apiUrl}/users/me/${endpoint}`; } + if (searchMode === ResourceSearchMode.Component) { + params['filter[parent][ne]'] = null; + } + + if (searchMode === ResourceSearchMode.Root) { + params['filter[parent]'] = null; + } + return this.jsonApiService.get(url, params).pipe( map((response: MyResourcesResponseJsonApi) => ({ data: response.data.map((item: MyResourcesItemGetResponseJsonApi) => MyResourcesMapper.fromResponse(item)), diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8aa227335..380a419ad 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -422,6 +422,11 @@ "myPreprints": "My Preprints", "bookmarks": "Bookmarks" }, + "tabOptions": { + "all": "All", + "root": "Projects", + "component": "Components" + }, "bookmarks": { "emptyState": "You don't have any bookmarks. Click the bookmark icon on projects or registrations to add them here." }, From 1b7761523eff315d084b4e8474f7cb1e67cb785e Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 16 Oct 2025 10:59:34 +0300 Subject: [PATCH 2/2] fix(my-projects): updated styles --- src/app/features/my-projects/my-projects.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/my-projects/my-projects.component.html b/src/app/features/my-projects/my-projects.component.html index 725a1ac05..ac66040f3 100644 --- a/src/app/features/my-projects/my-projects.component.html +++ b/src/app/features/my-projects/my-projects.component.html @@ -28,7 +28,7 @@ } -
+