Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
de16800
WIP - profile summary download
rmathew1011 Oct 1, 2025
6b99e1e
WIP - added rest call to get individual by id and error logging.
rmathew1011 Oct 1, 2025
a87da83
fix: to restore the accidentally commented code
rmathew1011 Oct 1, 2025
4adc9b7
Incorporated PR suggestion
rmathew1011 Oct 2, 2025
ba5c4de
- Added id attributes to Select All and individual profile checkboxes;
rmathew1011 Oct 6, 2025
a71b825
Removed the unused code and reverted back to the original state
rmathew1011 Oct 6, 2025
221cf7e
Update src/app/+data-and-analytics/data-and-analytics.component.ts
rmathew1011 Oct 7, 2025
c6fa6fe
Merge pull request #493 from TAMULib/profile-summary-feature-fix
rmathew1011 Oct 7, 2025
8d1ce03
Merge pull request #491 from TAMULib/profile-summary-download
rmathew1011 Oct 7, 2025
0618ee9
WIP - filename fix in progress
rmathew1011 Oct 7, 2025
afaa0c5
WIP - moved download code block to profile summary component
rmathew1011 Oct 10, 2025
9b00d15
- Moved download functionality from Data Analytics component to Prof…
rmathew1011 Oct 13, 2025
3ccc66e
Removed unused dependency from DataAnalytics component
rmathew1011 Oct 14, 2025
2770439
WIP - to add export to section view
rmathew1011 Oct 17, 2025
ebc2fc2
WIP - added the export button to the front end
rmathew1011 Oct 17, 2025
96204eb
Merge pull request #495 from TAMULib/sprint12-staging-filename-fix
rmathew1011 Oct 20, 2025
2199ce2
WIP - export feature is cuurently downloading a people csv - but cont…
rmathew1011 Oct 21, 2025
238d870
Merge remote-tracking branch 'origin/sprint12-staging' into test-expo…
rmathew1011 Oct 22, 2025
8cf3e3f
Code clean up
rmathew1011 Oct 23, 2025
765294f
Removed unused input
rmathew1011 Oct 23, 2025
0459927
Updated the dependency version for scholars-embed-utilities
rmathew1011 Oct 24, 2025
acc12dd
Merge pull request #499 from TAMULib/480-mailing-address-line-break
rmathew1011 Oct 24, 2025
5acef2f
Incorporated PR suggestions:
rmathew1011 Oct 27, 2025
1edd38a
Update src/app/+display/section/section.component.ts
rmathew1011 Oct 28, 2025
3bf4d73
Merge pull request #498 from TAMULib/470-export-section-people
rmathew1011 Oct 28, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"file-saver": "2.0.5",
"font-awesome": "4.7.0",
"rxjs": "7.8.1",
"scholars-embed-utilities": "0.4.2",
"scholars-embed-utilities": "0.5.0",
"tslib": "2.6.2",
"uuid": "9.0.1",
"zone.js": "0.14.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@
cursor: pointer;
}
}

}
15 changes: 12 additions & 3 deletions src/app/+data-and-analytics/data-and-analytics.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Store, select } from '@ngrx/store';
import { BehaviorSubject, Observable, OperatorFunction, combineLatest, debounceTime, distinctUntilChanged, filter, map, take, withLatestFrom } from 'rxjs';
import { BehaviorSubject, Observable, OperatorFunction, combineLatest, distinctUntilChanged, filter, map, take, withLatestFrom } from 'rxjs';

import { APP_CONFIG, AppConfig } from '../app.config';
import { Individual } from '../core/model/discovery';
import { IndividualRepo } from '../core/model/discovery/repo/individual.repo';
import { DataAndAnalyticsView, DisplayView, Filter, OpKey } from '../core/model/view';
Expand Down Expand Up @@ -57,19 +58,27 @@ export class DataAndAnalyticsComponent implements OnInit {

public organizations: Observable<Individual[]>;

public selectedPeopleSubject : BehaviorSubject<any[]>;

public get label(): Observable<string> {
return this.labelSubject.asObservable();
}

public get selectedPeople() : Observable<any[]> {
return this.selectedPeopleSubject.asObservable();
}

constructor(
@Inject(APP_CONFIG) private appConfig: AppConfig,
private router: Router,
private route: ActivatedRoute,
private store: Store<AppState>,
private individualRepo: IndividualRepo,
private individualRepo: IndividualRepo
) {
this.labelSubject = new BehaviorSubject<string>('');
this.organizationsSubject = new BehaviorSubject<Individual[]>([]);
this.organizations = this.organizationsSubject.asObservable();
this.selectedPeopleSubject = new BehaviorSubject<any[]>([]);
this.model = {
term: '',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
<div class="container mb-1" *ngIf="getSelectedExportView() | async; let selected">
<button class="btn btn-primary pull-right" (click)="download(organization, selected)">{{ 'DATA_AND_ANALYTICS.DOWNLOAD' | translate }}</button>
</div>
<div class="container mb-1 selectedProfile" *ngIf="getSelectedExportView() | async; let selected">

<div class="col-12" *ngIf="organization as organization">
<div class="mb-1" *ngIf="organization.people && organization.people.length > 0">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-check">
<input
id="select-all-profile"
type="checkbox"
class="form-check-input"
[checked]="(selectedPeople | async)?.length === organization.people.length"
(change)="onSelectAll($event, organization)"
>
<label class="form-check-label" for="select-all-profile">
{{ 'DATA_AND_ANALYTICS.SELECT_ALL' | translate }}
</label>
</div>
<button
class="btn btn-primary downloadSelectedPeople"
type="button"
[disabled]="!(selectedPeople | async)?.length"
(click)="downloadSelectedPeople(organization, selected)">
{{ 'DATA_AND_ANALYTICS.DOWNLOAD' | translate }}
</button>
</div>
<ul>
<li *ngFor="let person of organization.people">
<div class="form-check">
<input
id="selected-profile"
type="checkbox"
class="form-check-input selected-profile-checkbox"
[checked]="(selectedPeople | async)?.person"
(change)="onSelectPerson($event, person)"
/>
<span class="font-weight-normal">{{ person.label }} - {{ person.title }}</span>
</div>
</li>
</ul>
<input type="hidden" [value]="(selectedPeople | async) | json" name="selectedPeople" />
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@



:host {
.selectedProfile {
padding: 10px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.selectedProfile ul {
list-style-type: none;
padding-left: 0;
}
.selectedProfile ul li {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.selectedProfile .form-check {
display: flex;
align-items: center;
}
.selectedProfile .font-weight-normal {
line-height: 1.5;
margin-left: 10px;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, Inject, OnDestroy, OnInit, Output } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, Subscription, take } from 'rxjs';

import { APP_CONFIG, AppConfig } from '../../app.config';
import { Individual } from '../../core/model/discovery';
import { SidebarItemType, SidebarMenu } from '../../core/model/sidebar';
import { DataAndAnalyticsView, DisplayView, ExportView } from '../../core/model/view';
Expand Down Expand Up @@ -38,14 +39,32 @@ export class ProfileSummariesExportComponent implements OnDestroy, OnInit {

private subscriptions: Subscription[];

public selectedOrganization: Observable<Individual>;

public organizationsSubject: BehaviorSubject<Individual[]>;

public organizations: Observable<Individual[]>;

public selectedPeopleSubject : BehaviorSubject<any[]>;

public get selectedPeople() : Observable<any[]> {
return this.selectedPeopleSubject.asObservable();
}

constructor(
@Inject(APP_CONFIG) private appConfig: AppConfig,
private store: Store<AppState>,
private route: ActivatedRoute,
private translate: TranslateService,
private rest: RestService,
private restService: RestService,
) {
this.labelEvent = new EventEmitter<string>();
this.selectedPeopleSubject = new BehaviorSubject<any[]>([]);
this.organizationsSubject = new BehaviorSubject<Individual[]>([]);
this.organizations = this.organizationsSubject.asObservable();

this.subscriptions = [];

}

ngOnDestroy(): void {
Expand All @@ -66,7 +85,6 @@ export class ProfileSummariesExportComponent implements OnDestroy, OnInit {
title: this.translate.instant('DATA_AND_ANALYTICS.TIME_PERIOD'),
items: this.displayView.exportViews.map((exportView: ExportView) => {
const selected = exportView.name === queryParams.export;

if (selected) {
this.selectedExportView.next(exportView);
this.labelEvent.next(this.translate.instant('DATA_AND_ANALYTICS.PROFILE_SUMMARIES', { timePeriod: exportView.name }));
Expand All @@ -92,29 +110,90 @@ export class ProfileSummariesExportComponent implements OnDestroy, OnInit {
this.store.dispatch(new fromSidebar.LoadSidebarAction({ menu }));
})
);

}

public getSelectedExportView(): Observable<ExportView> {
return this.selectedExportView.asObservable();
}

public download(organization: Individual, exportView: ExportView): void {
const link = exportView.name.toLowerCase().replace(/ /g, '_');
this.rest.get<Blob>(organization._links[link].href, { observe: 'response', responseType: 'blob' as 'json' })
.pipe(take(1))
.subscribe((response: any) => {
const contentDisposition = response.headers.get('Content-Disposition');
const filename = !!contentDisposition
? contentDisposition.match(/^.*filename=(.*)$/)[1]
: 'export.zip';


const url = window.URL.createObjectURL(response.body);
const anchor = document.createElement('a');
anchor.download = filename;
anchor.href = url;
anchor.click();
});
public onSelectAll(event: Event, organization: any): void {
const checked = (event.target as HTMLInputElement).checked;
const people = organization.people || [];
if (checked) {
this.selectedPeopleSubject.next([...people]);
} else {
this.selectedPeopleSubject.next([]);
}
const checkboxes = document.querySelectorAll<HTMLInputElement>('.selected-profile-checkbox');
checkboxes.forEach(cb => cb.checked = checked);
}

public onSelectPerson(event: Event, person: Individual): void {
const checked = (event.target as HTMLInputElement).checked;
if (checked) {
const current = this.selectedPeopleSubject.value;
if (!current.find(p => p.id === person.id)) {
this.selectedPeopleSubject.next([...current, person]);
}
} else {
const current = this.selectedPeopleSubject.value.filter(p => p.id !== person.id);
this.selectedPeopleSubject.next(current);
}
}

private extractFilename(response: any, defaultFileName: string): string {
const contentDisposition = response.headers?.get('Content-Disposition');
return contentDisposition?.match(/^.*filename=(.*)$/)[1]?.trim() || defaultFileName;
}

public downloadSelectedPeople(organization: any, selected: any): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const orgId = params?.selectedOrganization ? params.selectedOrganization : organization.id;
const selectedIds = this.selectedPeopleSubject.value.map(p => p.id);

if (!orgId) {
console.error('Download failure: Missing Organization id.');
return;
}

if (!selectedIds.length || selectedIds.length === (organization.people?.length ?? 0)) {
const link = params?.export.toLowerCase().replace(/ /g, '_');
this.restService.get<Blob>(
organization._links[link].href,
{ observe: 'response', responseType: 'blob' as 'json' })
.pipe(take(1))
.subscribe((response: any) => {
const filename = this.extractFilename(response, 'export.zip');
this.download(response, filename);
},);
} else {
const exportName = (selected?.name ? selected.name : params?.export ? params.export : '')
.trim().replace(/\s+/g, ' ');
const updatedHref = `${this.appConfig.serviceUrl}/individual/${orgId}/export?type=zip&name=${encodeURIComponent(exportName)}`;
this.restService.post(
updatedHref,
selectedIds,
{ observe: 'response', responseType: 'blob' as 'json', headers: { 'Content-Type': 'application/json' } }
).subscribe({
next: (response: any) => {
const filename = this.extractFilename(response, 'selected_profile.zip');
this.download(response, filename);
},
error: (err) => console.error('Failed to download selected profiles.', err),
});
}
});
}

private download(response: any, filename: any): void {
const blob = response.body || response;
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.download = filename;
anchor.href = url;
anchor.click();
window.URL.revokeObjectURL(url);
}

}
20 changes: 18 additions & 2 deletions src/app/+display/section/section.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
<div class="card-header font-weight-bold text-primary text-capitalize">
<span>{{ section.name }}</span>
<div class="float-right" *ngIf="section.shared">
<div *ngIf="queryParams | async; let queryParams">
<div class="container mt-2" >
<div class="headers-row row flex-column-reverse flex-md-row">
<div class="col-md-8">
<span id="sidebar-title">{{ section.name }}</span>
</div>
<div class="col-md-4 text-right">
<span *ngIf="hasExport(section)" class="column-export">
<a [href]="getSectionExportUrl(queryParams, section)" download class="btn">
<span class="fa fa-share" [attr.aria-hidden]="true"></span>
<span>{{ 'DIRECTORY.EXPORT' | translate }}</span>
</a>
</span>
</div>
</div>
</div>
</div>
<div class="float-right" *ngIf="section.shared">
<div class="embed-dropdown d-inline-block" placement="bottom-right" ngbDropdown>
<i class="fa fa-lg fa-share-alt" ngbDropdownToggle ></i>
<div class="dropdown-menu" ngbDropdownMenu>
Expand Down
23 changes: 23 additions & 0 deletions src/app/+display/section/section.component.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
:host {
width: 100%;
.headers-row {
.column-export {
.btn {
color: var(--sidebar-button-color);
background-color: var(--sidebar-button-background-color);
border: 1px solid var(--sidebar-button-border-color);
font-size: 0.9em;
padding: 5px 10px 4px;
.fa {
padding-right: 0.3em;
}
}
.btn:active,
.btn:hover {
color: var(--sidebar-button-hover-color);
background-color: var(--sidebar-button-hover-background-color);
}
.btn:focus {
box-shadow: 0 0 0 0.2rem var(--sidebar-button-focus-shadow-color);
}
}
}
.embed-dropdown {
.dropdown-menu {
min-width: 450px;
Expand Down
Loading