Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0cc7430
chore(157): project-addons-ui
kpetrov24exoft May 28, 2025
9797cb9
fix(project-addons): minor fixes
kpetrov24exoft Jun 2, 2025
ddd68fc
Merge branch 'main' into feat/184-project-addons-api
rnastyuk Jun 3, 2025
1e279f1
feat(project-addons-api): moved reusable addon files to the shared fo…
rnastyuk Jun 3, 2025
4d92a72
Merge branch 'main' into feat/184-project-addons-api
rnastyuk Jun 3, 2025
9cb147b
feat(profile-addons-api): added initial profile addons page structure…
rnastyuk Jun 9, 2025
988e267
Merge branch 'refs/heads/main' into feat/184-project-addons-api
rnastyuk Jun 9, 2025
2ccd258
feat(profile-addons-api): added addon configuration functionality
rnastyuk Jun 16, 2025
905ab39
Merge branch 'main' into feat/184-project-addons-api
rnastyuk Jun 16, 2025
3484429
feat(profile-addons-api): fixed connection creation bug, fixed form, …
rnastyuk Jun 18, 2025
614d12d
Merge branch 'main' into feat/184-project-addons-api
rnastyuk Jun 18, 2025
4afb7a6
feat(profile-addons-api): added breadcrumbs trimming
rnastyuk Jun 18, 2025
b040475
Merge branch 'main' into feat/184-project-addons-api
rnastyuk Jun 18, 2025
8c21899
feat(profile-addons-api): fixed comments
rnastyuk Jun 18, 2025
cd6365a
feat(profile-addons-api): changed p-select usage to reusable osf-select
rnastyuk Jun 19, 2025
3377ef7
Merge branch 'main' into feat/178-collections-api
rnastyuk Jun 20, 2025
52ef634
feat(collections-api): added get collection provider logic
rnastyuk Jun 20, 2025
b7e8669
feat(profile-addons-api): added filter logic and collections submissi…
rnastyuk Jun 25, 2025
8382bdf
feat(profile-addons-api): added dynamic rendering of the collections …
rnastyuk Jun 26, 2025
0a98a99
Merge branch 'main' into feat/178-collections-api
rnastyuk Jun 26, 2025
b7e2dd4
feat(collections-api): refactored branding service
rnastyuk Jun 27, 2025
8e765b7
feat(collections-api): removed unused filter property
rnastyuk Jul 1, 2025
fb73cd8
feat(collections-api): added clear collection submissions action
rnastyuk Jul 1, 2025
0741bd4
Merge branch 'main' into feat/178-collections-api
rnastyuk Jul 1, 2025
ecc5e92
Merge branch 'refs/heads/main' into feat/178-collections-api
rnastyuk Jul 1, 2025
40e8ff4
feat(profile-addons-api): resolved merge conflicts
rnastyuk Jul 1, 2025
ec7d619
Merge branch 'main' into feat/178-collections-api
rnastyuk Jul 1, 2025
527855a
feat(collections-api): moved collections routes file out of the const…
rnastyuk Jul 1, 2025
b80735f
feat(collections-api): fixed comments
rnastyuk Jul 2, 2025
1809cff
Merge branch 'main' into feat/178-collections-api
rnastyuk Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/core/components/nav-menu/nav-menu.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<nav class="nav-menu">
<p-panelMenu [model]="mainMenuItems" [multiple]="false">
<p-panelMenu [model]="mainMenuItems()" [multiple]="false">
<ng-template #item let-item>
<a
[routerLink]="item.routerLink"
Expand Down
25 changes: 21 additions & 4 deletions src/app/core/components/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export class NavMenuComponent {
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));

closeMenu = output<void>();

Expand All @@ -40,8 +39,17 @@ export class NavMenuComponent {

protected readonly currentProjectId = computed(() => this.currentRoute().projectId);
protected readonly isProjectRoute = computed(() => !!this.currentProjectId());
protected readonly isCollectionsRoute = computed(() => this.currentRoute().isCollectionsWithId);

convertToMenuItem(item: NavItem): MenuItem {
protected readonly mainMenuItems = computed(() => {
const filteredItems = this.isCollectionsRoute()
? this.navItems
: this.navItems.filter((item) => item.path !== '/collections');

return filteredItems.map((item) => this.convertToMenuItem(item));
});

private convertToMenuItem(item: NavItem): MenuItem {
const currentUrl = this.router.url;
const isExpanded =
item.isCollapsible &&
Expand All @@ -57,11 +65,20 @@ export class NavMenuComponent {
};
}

getRouteInfo() {
private getRouteInfo() {
const url = this.router.url;
const urlSegments = url.split('/').filter((segment) => segment);

const projectId = this.route.firstChild?.snapshot.params['id'] || null;
const section = this.route.firstChild?.firstChild?.snapshot.url[0]?.path || 'overview';

return { projectId, section };
const isCollectionsWithId = urlSegments[0] === 'collections' && urlSegments[1] && urlSegments[1] !== '';

return {
projectId,
section,
isCollectionsWithId,
};
}

goToLink(item: MenuItem) {
Expand Down
8 changes: 3 additions & 5 deletions src/app/core/services/json-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ export class JsonApiService {
}

post<T>(url: string, body?: unknown, params?: Record<string, unknown>): Observable<T> {
return this.http
.post<JsonApiResponse<T, null>>(url, body, {
params: this.buildHttpParams(params),
})
.pipe(map((response) => response.data));
return this.http.post<T>(url, body, {
params: this.buildHttpParams(params),
});
}

patch<T>(url: string, body: unknown): Observable<T> {
Expand Down
50 changes: 30 additions & 20 deletions src/app/features/collections/collections.component.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
<section class="collections flex flex-column flex-1 mt-0 sm:mt-5 xl:mt-7">
<div class="collections-sub-header flex justify-content-between flex-column gap-4 mb-4 sm:mb-6 sm:gap-0 sm:flex-row">
<div class="flex gap-3">
<i class="collections-icon text-white osf-icon-collections"></i>
<h1 class="flex align-items-center text-white">Collection Title</h1>
</div>
@if (!isProviderLoading()) {
<section class="collections flex flex-column flex-1 pt-0 xl:pt-5">
<div
class="collections-sub-header flex justify-content-between flex-column gap-4 mb-4 sm:mb-6 sm:gap-0 sm:flex-row"
>
<div class="flex gap-3">
<i class="collections-icon text-white osf-icon-collections"></i>
<h1 class="flex align-items-center text-white">{{ collectionProvider()?.name }}</h1>
</div>

<p-button class="collections-heading-btn" [label]="'collections.buttons.addToCollection' | translate" />
</div>
<p-button class="collections-heading-btn" [label]="'collections.buttons.addToCollection' | translate" />
</div>

<div class="search-input-container">
<osf-search-input
[control]="searchControl"
[showHelpIcon]="true"
[placeholder]="'collections.searchInput.placeholder' | translate"
(helpClicked)="openHelpDialog()"
/>
</div>
<div class="search-input-container">
<osf-search-input
[control]="searchControl"
[showHelpIcon]="true"
[placeholder]="'collections.searchInput.placeholder' | translate"
(helpClicked)="openHelpDialog()"
(triggerSearch)="onSearchTriggered($event)"
/>
@if (collectionProvider()?.description) {
<div class="mt-6 text-white" [innerHTML]="collectionProvider()?.description"></div>
}
</div>

<div class="content-container flex-1">
<osf-collections-main-content />
</div>
</section>
<div class="content-container flex-1">
<osf-collections-main-content />
</div>
</section>
} @else {
<osf-loading-spinner />
}
46 changes: 22 additions & 24 deletions src/app/features/collections/collections.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,36 @@

:host {
--collection-bg-color: #013b5c;
display: flex;
flex-direction: column;
@include mix.flex-column;
flex: 1;
}

.collections {
background: var(--collection-bg-color);
border: 2px solid var.$white;
border-top: none;
.collections {
background: var(--collection-bg-color);
border-top: none;

.collections-sub-header {
margin: mix.rem(48px) mix.rem(28px);
.collections-sub-header {
margin: mix.rem(48px) mix.rem(28px);

.collections-icon {
font-size: mix.rem(42px);
}
.collections-icon {
font-size: mix.rem(42px);
}
}

.search-input-container {
margin: 0 mix.rem(28px) mix.rem(48px) mix.rem(28px);
position: relative;
.search-input-container {
margin: 0 mix.rem(28px) mix.rem(48px) mix.rem(28px);
position: relative;

img {
position: absolute;
right: mix.rem(4px);
top: mix.rem(4px);
z-index: 1;
}
img {
position: absolute;
right: mix.rem(4px);
top: mix.rem(4px);
z-index: 1;
}
}

.content-container {
background: var.$white;
padding: mix.rem(28px);
}
.content-container {
background: var.$white;
padding: mix.rem(28px);
}
}
154 changes: 147 additions & 7 deletions src/app/features/collections/collections.component.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,77 @@
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe, TranslateService } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { DialogService } from 'primeng/dynamicdialog';

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { debounceTime } from 'rxjs';

import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';

import { CollectionsHelpDialogComponent, CollectionsMainContentComponent } from '@osf/features/collections/components';
import { SearchInputComponent } from '@shared/components';
import { CollectionsFilters } from '@osf/features/collections/models';
import { CollectionsQuerySyncService } from '@osf/features/collections/services';
import {
ClearCollections,
ClearCollectionSubmissions,
CollectionsSelectors,
GetCollectionDetails,
GetCollectionProvider,
GetCollectionSubmissions,
SetPageNumber,
SetSearchValue,
} from '@osf/features/collections/store';
import { LoadingSpinnerComponent, SearchInputComponent } from '@shared/components';

@Component({
selector: 'osf-collections',
imports: [SearchInputComponent, TranslatePipe, Button, CollectionsMainContentComponent],
imports: [SearchInputComponent, TranslatePipe, Button, CollectionsMainContentComponent, LoadingSpinnerComponent],
templateUrl: './collections.component.html',
styleUrl: './collections.component.scss',
providers: [DialogService],
providers: [DialogService, CollectionsQuerySyncService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CollectionsComponent {
protected dialogService = inject(DialogService);
protected translateService = inject(TranslateService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private dialogService = inject(DialogService);
private translateService = inject(TranslateService);
private querySyncService = inject(CollectionsQuerySyncService);
private destroyRef = inject(DestroyRef);

protected searchControl = new FormControl('');
protected providerId = signal<string>('');

protected collectionProvider = select(CollectionsSelectors.getCollectionProvider);
protected collectionDetails = select(CollectionsSelectors.getCollectionDetails);
protected selectedFilters = select(CollectionsSelectors.getAllSelectedFilters);
protected sortBy = select(CollectionsSelectors.getSortBy);
protected searchText = select(CollectionsSelectors.getSearchText);
protected pageNumber = select(CollectionsSelectors.getPageNumber);
protected isProviderLoading = select(CollectionsSelectors.getCollectionProviderLoading);
protected primaryCollectionId = computed(() => this.collectionProvider()?.primaryCollection?.id);

openHelpDialog() {
protected actions = createDispatchMap({
getCollectionProvider: GetCollectionProvider,
getCollectionDetails: GetCollectionDetails,
setSearchValue: SetSearchValue,
getCollectionSubmissions: GetCollectionSubmissions,
setPageNumber: SetPageNumber,
clearCollections: ClearCollections,
clearCollectionsSubmissions: ClearCollectionSubmissions,
});

constructor() {
this.initializeProvider();
this.setupEffects();
this.setupSearchBinding();
}

protected openHelpDialog(): void {
this.dialogService.open(CollectionsHelpDialogComponent, {
focusOnShow: false,
header: this.translateService.instant('collections.helpDialog.header'),
Expand All @@ -31,4 +80,95 @@ export class CollectionsComponent {
closable: true,
});
}

protected onSearchTriggered(searchValue: string): void {
this.actions.setSearchValue(searchValue);
this.actions.setPageNumber('1');
}

private initializeProvider(): void {
const id = this.route.snapshot.paramMap.get('id');
if (!id) {
this.router.navigate(['/not-found']);
return;
}

this.providerId.set(id);
this.actions.getCollectionProvider(id);
}

private setupEffects(): void {
this.querySyncService.initializeFromUrl();

effect(() => {
const collectionId = this.primaryCollectionId();
if (collectionId) {
this.actions.getCollectionDetails(collectionId);
}
});

effect(() => {
const searchText = this.searchText();
const sortBy = this.sortBy();
const selectedFilters = this.selectedFilters();
const pageNumber = this.pageNumber();

if (searchText !== undefined && sortBy !== undefined && selectedFilters && pageNumber) {
this.querySyncService.syncStoreToUrl(searchText, sortBy, selectedFilters, pageNumber);
}
});

effect(() => {
const searchText = this.searchText();
const sortBy = this.sortBy();
const selectedFilters = this.selectedFilters();
const pageNumber = this.pageNumber();
const providerId = this.providerId();
const collectionDetails = this.collectionDetails();

if (searchText !== undefined && selectedFilters && pageNumber && providerId && collectionDetails) {
const activeFilters = this.getActiveFilters(selectedFilters);
this.actions.clearCollectionsSubmissions();
this.actions.getCollectionSubmissions(providerId, searchText, activeFilters, pageNumber, sortBy);
}
});

effect(() => {
this.destroyRef.onDestroy(() => {
this.actions.clearCollections();
});
});
}

private getActiveFilters(filters: CollectionsFilters): Record<string, string[]> {
return Object.entries(filters)
.filter(([key, value]) => value.length)
.reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<string, string[]>
);
}

private setupSearchBinding(): void {
effect(() => {
const storeSearchText = this.searchText();
const currentControlValue = this.searchControl.value;

if (storeSearchText !== currentControlValue) {
this.searchControl.setValue(storeSearchText, { emitEvent: false });
}
});

this.searchControl.valueChanges
.pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
.subscribe((searchValue) => {
const trimmedValue = searchValue?.trim() || '';
if (trimmedValue !== this.searchText()) {
this.actions.setSearchValue(trimmedValue);
}
});
}
}
14 changes: 10 additions & 4 deletions src/app/features/collections/collections.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { provideStates } from '@ngxs/store';

import { Routes } from '@angular/router';

import { CollectionsComponent } from '@osf/features/collections/collections.component';

import { ModerationState } from '../moderation/store';

export const collectionsRoutes: Routes = [
Expand All @@ -13,10 +11,18 @@ export const collectionsRoutes: Routes = [
{
path: '',
pathMatch: 'full',
component: CollectionsComponent,
loadComponent: () =>
import('@core/components/page-not-found/page-not-found.component').then((mod) => mod.PageNotFoundComponent),
data: { skipBreadcrumbs: true },
},
{
path: ':id',
pathMatch: 'full',
loadComponent: () =>
import('@osf/features/collections/collections.component').then((mod) => mod.CollectionsComponent),
},
{
path: 'moderation',
path: ':id/moderation',
loadComponent: () =>
import('@osf/features/moderation/pages/collection-moderation/collection-moderation.component').then(
(m) => m.CollectionModerationComponent
Expand Down
Loading