Skip to content

Commit defe44e

Browse files
authored
Merge pull request #494 from TAMULib/sprint12-staging
Sprint12 staging
2 parents 3944069 + 3bf4d73 commit defe44e

File tree

12 files changed

+262
-34
lines changed

12 files changed

+262
-34
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"file-saver": "2.0.5",
6060
"font-awesome": "4.7.0",
6161
"rxjs": "7.8.1",
62-
"scholars-embed-utilities": "0.4.2",
62+
"scholars-embed-utilities": "0.5.0",
6363
"tslib": "2.6.2",
6464
"uuid": "9.0.1",
6565
"zone.js": "0.14.0"

src/app/+data-and-analytics/data-and-analytics.component.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@
3030
cursor: pointer;
3131
}
3232
}
33+
3334
}

src/app/+data-and-analytics/data-and-analytics.component.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
22
import { ActivatedRoute, Params, Router } from '@angular/router';
33
import { Store, select } from '@ngrx/store';
4-
import { BehaviorSubject, Observable, OperatorFunction, combineLatest, debounceTime, distinctUntilChanged, filter, map, take, withLatestFrom } from 'rxjs';
4+
import { BehaviorSubject, Observable, OperatorFunction, combineLatest, distinctUntilChanged, filter, map, take, withLatestFrom } from 'rxjs';
55

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

5859
public organizations: Observable<Individual[]>;
5960

61+
public selectedPeopleSubject : BehaviorSubject<any[]>;
62+
6063
public get label(): Observable<string> {
6164
return this.labelSubject.asObservable();
6265
}
6366

67+
public get selectedPeople() : Observable<any[]> {
68+
return this.selectedPeopleSubject.asObservable();
69+
}
70+
6471
constructor(
72+
@Inject(APP_CONFIG) private appConfig: AppConfig,
6573
private router: Router,
6674
private route: ActivatedRoute,
6775
private store: Store<AppState>,
68-
private individualRepo: IndividualRepo,
76+
private individualRepo: IndividualRepo
6977
) {
7078
this.labelSubject = new BehaviorSubject<string>('');
7179
this.organizationsSubject = new BehaviorSubject<Individual[]>([]);
7280
this.organizations = this.organizationsSubject.asObservable();
81+
this.selectedPeopleSubject = new BehaviorSubject<any[]>([]);
7382
this.model = {
7483
term: '',
7584
};
Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,43 @@
1-
<div class="container mb-1" *ngIf="getSelectedExportView() | async; let selected">
2-
<button class="btn btn-primary pull-right" (click)="download(organization, selected)">{{ 'DATA_AND_ANALYTICS.DOWNLOAD' | translate }}</button>
3-
</div>
1+
<div class="container mb-1 selectedProfile" *ngIf="getSelectedExportView() | async; let selected">
2+
3+
<div class="col-12" *ngIf="organization as organization">
4+
<div class="mb-1" *ngIf="organization.people && organization.people.length > 0">
5+
<div class="d-flex justify-content-between align-items-center mb-2">
6+
<div class="form-check">
7+
<input
8+
id="select-all-profile"
9+
type="checkbox"
10+
class="form-check-input"
11+
[checked]="(selectedPeople | async)?.length === organization.people.length"
12+
(change)="onSelectAll($event, organization)"
13+
>
14+
<label class="form-check-label" for="select-all-profile">
15+
{{ 'DATA_AND_ANALYTICS.SELECT_ALL' | translate }}
16+
</label>
17+
</div>
18+
<button
19+
class="btn btn-primary downloadSelectedPeople"
20+
type="button"
21+
[disabled]="!(selectedPeople | async)?.length"
22+
(click)="downloadSelectedPeople(organization, selected)">
23+
{{ 'DATA_AND_ANALYTICS.DOWNLOAD' | translate }}
24+
</button>
25+
</div>
26+
<ul>
27+
<li *ngFor="let person of organization.people">
28+
<div class="form-check">
29+
<input
30+
id="selected-profile"
31+
type="checkbox"
32+
class="form-check-input selected-profile-checkbox"
33+
[checked]="(selectedPeople | async)?.person"
34+
(change)="onSelectPerson($event, person)"
35+
/>
36+
<span class="font-weight-normal">{{ person.label }} - {{ person.title }}</span>
37+
</div>
38+
</li>
39+
</ul>
40+
<input type="hidden" [value]="(selectedPeople | async) | json" name="selectedPeople" />
41+
</div>
42+
</div>
43+
</div>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
3+
4+
:host {
5+
.selectedProfile {
6+
padding: 10px;
7+
background-color: #f9f9f9;
8+
border-radius: 8px;
9+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
10+
}
11+
.selectedProfile ul {
12+
list-style-type: none;
13+
padding-left: 0;
14+
}
15+
.selectedProfile ul li {
16+
display: flex;
17+
justify-content: space-between;
18+
align-items: center;
19+
margin-bottom: 12px;
20+
}
21+
.selectedProfile .form-check {
22+
display: flex;
23+
align-items: center;
24+
}
25+
.selectedProfile .font-weight-normal {
26+
line-height: 1.5;
27+
margin-left: 10px;
28+
}
29+
}

src/app/+data-and-analytics/profile-summaries-export/profile-summaries-export.component.ts

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
1+
import { Component, EventEmitter, Input, Inject, OnDestroy, OnInit, Output } from '@angular/core';
22
import { ActivatedRoute, Params } from '@angular/router';
33
import { Store } from '@ngrx/store';
44
import { TranslateService } from '@ngx-translate/core';
55
import { BehaviorSubject, Observable, Subscription, take } from 'rxjs';
66

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

3940
private subscriptions: Subscription[];
4041

42+
public selectedOrganization: Observable<Individual>;
43+
44+
public organizationsSubject: BehaviorSubject<Individual[]>;
45+
46+
public organizations: Observable<Individual[]>;
47+
48+
public selectedPeopleSubject : BehaviorSubject<any[]>;
49+
50+
public get selectedPeople() : Observable<any[]> {
51+
return this.selectedPeopleSubject.asObservable();
52+
}
53+
4154
constructor(
55+
@Inject(APP_CONFIG) private appConfig: AppConfig,
4256
private store: Store<AppState>,
4357
private route: ActivatedRoute,
4458
private translate: TranslateService,
45-
private rest: RestService,
59+
private restService: RestService,
4660
) {
4761
this.labelEvent = new EventEmitter<string>();
62+
this.selectedPeopleSubject = new BehaviorSubject<any[]>([]);
63+
this.organizationsSubject = new BehaviorSubject<Individual[]>([]);
64+
this.organizations = this.organizationsSubject.asObservable();
65+
4866
this.subscriptions = [];
67+
4968
}
5069

5170
ngOnDestroy(): void {
@@ -66,7 +85,6 @@ export class ProfileSummariesExportComponent implements OnDestroy, OnInit {
6685
title: this.translate.instant('DATA_AND_ANALYTICS.TIME_PERIOD'),
6786
items: this.displayView.exportViews.map((exportView: ExportView) => {
6887
const selected = exportView.name === queryParams.export;
69-
7088
if (selected) {
7189
this.selectedExportView.next(exportView);
7290
this.labelEvent.next(this.translate.instant('DATA_AND_ANALYTICS.PROFILE_SUMMARIES', { timePeriod: exportView.name }));
@@ -92,29 +110,90 @@ export class ProfileSummariesExportComponent implements OnDestroy, OnInit {
92110
this.store.dispatch(new fromSidebar.LoadSidebarAction({ menu }));
93111
})
94112
);
113+
95114
}
96115

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

101-
public download(organization: Individual, exportView: ExportView): void {
102-
const link = exportView.name.toLowerCase().replace(/ /g, '_');
103-
this.rest.get<Blob>(organization._links[link].href, { observe: 'response', responseType: 'blob' as 'json' })
104-
.pipe(take(1))
105-
.subscribe((response: any) => {
106-
const contentDisposition = response.headers.get('Content-Disposition');
107-
const filename = !!contentDisposition
108-
? contentDisposition.match(/^.*filename=(.*)$/)[1]
109-
: 'export.zip';
110-
111-
112-
const url = window.URL.createObjectURL(response.body);
113-
const anchor = document.createElement('a');
114-
anchor.download = filename;
115-
anchor.href = url;
116-
anchor.click();
117-
});
120+
public onSelectAll(event: Event, organization: any): void {
121+
const checked = (event.target as HTMLInputElement).checked;
122+
const people = organization.people || [];
123+
if (checked) {
124+
this.selectedPeopleSubject.next([...people]);
125+
} else {
126+
this.selectedPeopleSubject.next([]);
127+
}
128+
const checkboxes = document.querySelectorAll<HTMLInputElement>('.selected-profile-checkbox');
129+
checkboxes.forEach(cb => cb.checked = checked);
130+
}
131+
132+
public onSelectPerson(event: Event, person: Individual): void {
133+
const checked = (event.target as HTMLInputElement).checked;
134+
if (checked) {
135+
const current = this.selectedPeopleSubject.value;
136+
if (!current.find(p => p.id === person.id)) {
137+
this.selectedPeopleSubject.next([...current, person]);
138+
}
139+
} else {
140+
const current = this.selectedPeopleSubject.value.filter(p => p.id !== person.id);
141+
this.selectedPeopleSubject.next(current);
142+
}
143+
}
144+
145+
private extractFilename(response: any, defaultFileName: string): string {
146+
const contentDisposition = response.headers?.get('Content-Disposition');
147+
return contentDisposition?.match(/^.*filename=(.*)$/)[1]?.trim() || defaultFileName;
148+
}
149+
150+
public downloadSelectedPeople(organization: any, selected: any): void {
151+
this.route.queryParams.pipe(take(1)).subscribe((params) => {
152+
const orgId = params?.selectedOrganization ? params.selectedOrganization : organization.id;
153+
const selectedIds = this.selectedPeopleSubject.value.map(p => p.id);
154+
155+
if (!orgId) {
156+
console.error('Download failure: Missing Organization id.');
157+
return;
158+
}
159+
160+
if (!selectedIds.length || selectedIds.length === (organization.people?.length ?? 0)) {
161+
const link = params?.export.toLowerCase().replace(/ /g, '_');
162+
this.restService.get<Blob>(
163+
organization._links[link].href,
164+
{ observe: 'response', responseType: 'blob' as 'json' })
165+
.pipe(take(1))
166+
.subscribe((response: any) => {
167+
const filename = this.extractFilename(response, 'export.zip');
168+
this.download(response, filename);
169+
},);
170+
} else {
171+
const exportName = (selected?.name ? selected.name : params?.export ? params.export : '')
172+
.trim().replace(/\s+/g, ' ');
173+
const updatedHref = `${this.appConfig.serviceUrl}/individual/${orgId}/export?type=zip&name=${encodeURIComponent(exportName)}`;
174+
this.restService.post(
175+
updatedHref,
176+
selectedIds,
177+
{ observe: 'response', responseType: 'blob' as 'json', headers: { 'Content-Type': 'application/json' } }
178+
).subscribe({
179+
next: (response: any) => {
180+
const filename = this.extractFilename(response, 'selected_profile.zip');
181+
this.download(response, filename);
182+
},
183+
error: (err) => console.error('Failed to download selected profiles.', err),
184+
});
185+
}
186+
});
187+
}
188+
189+
private download(response: any, filename: any): void {
190+
const blob = response.body || response;
191+
const url = window.URL.createObjectURL(blob);
192+
const anchor = document.createElement('a');
193+
anchor.download = filename;
194+
anchor.href = url;
195+
anchor.click();
196+
window.URL.revokeObjectURL(url);
118197
}
119198

120199
}

src/app/+display/section/section.component.html

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
<div class="card-header font-weight-bold text-primary text-capitalize">
2-
<span>{{ section.name }}</span>
3-
<div class="float-right" *ngIf="section.shared">
2+
<div *ngIf="queryParams | async; let queryParams">
3+
<div class="container mt-2" >
4+
<div class="headers-row row flex-column-reverse flex-md-row">
5+
<div class="col-md-8">
6+
<span id="sidebar-title">{{ section.name }}</span>
7+
</div>
8+
<div class="col-md-4 text-right">
9+
<span *ngIf="hasExport(section)" class="column-export">
10+
<a [href]="getSectionExportUrl(queryParams, section)" download class="btn">
11+
<span class="fa fa-share" [attr.aria-hidden]="true"></span>
12+
<span>{{ 'DIRECTORY.EXPORT' | translate }}</span>
13+
</a>
14+
</span>
15+
</div>
16+
</div>
17+
</div>
18+
</div>
19+
<div class="float-right" *ngIf="section.shared">
420
<div class="embed-dropdown d-inline-block" placement="bottom-right" ngbDropdown>
521
<i class="fa fa-lg fa-share-alt" ngbDropdownToggle ></i>
622
<div class="dropdown-menu" ngbDropdownMenu>

src/app/+display/section/section.component.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
:host {
2+
width: 100%;
3+
.headers-row {
4+
.column-export {
5+
.btn {
6+
color: var(--sidebar-button-color);
7+
background-color: var(--sidebar-button-background-color);
8+
border: 1px solid var(--sidebar-button-border-color);
9+
font-size: 0.9em;
10+
padding: 5px 10px 4px;
11+
.fa {
12+
padding-right: 0.3em;
13+
}
14+
}
15+
.btn:active,
16+
.btn:hover {
17+
color: var(--sidebar-button-hover-color);
18+
background-color: var(--sidebar-button-hover-background-color);
19+
}
20+
.btn:focus {
21+
box-shadow: 0 0 0 0.2rem var(--sidebar-button-focus-shadow-color);
22+
}
23+
}
24+
}
225
.embed-dropdown {
326
.dropdown-menu {
427
min-width: 450px;

0 commit comments

Comments
 (0)