Skip to content

Commit 940097a

Browse files
authored
Add community grouping (#2664)
1 parent adc12d9 commit 940097a

14 files changed

+473
-307
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
script:
9595
- set -e
9696
- echo "Waiting for couchdb to start"; WAIT_TIME=0; until curl http://127.0.0.1:5984/_all_dbs || [ $WAIT_TIME -eq 180 ]; do echo "..."; sleep 5; WAIT_TIME=$(expr $WAIT_TIME + 5); done
97-
- i=$(curl -X GET http://127.0.0.1:5984/_all_dbs | jq length); if [ $i -ne 27 ]; then exit 1; fi
97+
- i=$(curl -X GET http://127.0.0.1:5984/_all_dbs | jq length); if [ $i -ne 28 ]; then exit 1; fi
9898
- ng e2e
9999
- stage: docker-release
100100
<<: *_prepare_deploy

couchdb-setup.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ curl -X PUT $COUCHURL/replicator_users
123123
curl -X PUT $COUCHURL/admin_activities
124124
curl -X PUT $COUCHURL/child_statistics
125125
curl -X PUT $COUCHURL/tags
126+
curl -X PUT $COUCHURL/hubs
126127

127128
# Create design documents
128129
node ./design/create-design-docs.js
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<mat-table #table [dataSource]="communities" matSort>
2+
<ng-container matColumnDef="name">
3+
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
4+
<mat-cell *matCellDef="let element">{{element.name}}</mat-cell>
5+
</ng-container>
6+
<ng-container matColumnDef="code">
7+
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>Code</mat-header-cell>
8+
<mat-cell *matCellDef="let element">{{element.code}}</mat-cell>
9+
</ng-container>
10+
<ng-container matColumnDef="localDomain">
11+
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>URL</mat-header-cell>
12+
<mat-cell *matCellDef="let element">{{element.localDomain}}</mat-cell>
13+
</ng-container>
14+
<ng-container matColumnDef="createdDate">
15+
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>Created Date</mat-header-cell>
16+
<mat-cell *matCellDef="let element">{{element.createdDate | date: 'mediumDate'}}</mat-cell>
17+
</ng-container>
18+
<ng-container matColumnDef="action">
19+
<mat-header-cell *matHeaderCellDef i18n>Actions</mat-header-cell>
20+
<mat-cell *matCellDef="let element">
21+
<button mat-raised-button color="primary" (click)="view(element)" i18n>View</button>
22+
<ng-container *ngIf="element.registrationRequest === 'accepted'">
23+
<button *ngIf="element.planetType === 'nation'" mat-raised-button color="primary" (click)="getChildPlanet(element.localDomain)" i18n>
24+
Communities
25+
</button>
26+
<button mat-raised-button color="primary" i18n [matMenuTriggerFor]="hubMenu" *ngIf="(hub==='sandbox' && hubs.length > 0) || hubs.length > 1">
27+
{ hub, select,
28+
sandbox {Add to { planetType, select, nation {Network} center {Region} }}
29+
other {Move { planetType, select, nation {Networks} center {Regions} }}
30+
}
31+
</button>
32+
<mat-menu #hubMenu="matMenu">
33+
<ng-container *ngFor="let h of hubs">
34+
<button *ngIf="h.name!==hub.name" (click)="addHubClick(element.code, h.name)" mat-menu-item>{{h.name}}</button>
35+
</ng-container>
36+
<button (click)="removeHubClick(element.code)" *ngIf="hub!=='sandbox'" mat-menu-item>Sandbox</button>
37+
</mat-menu>
38+
</ng-container>
39+
<ng-container *ngIf="element.registrationRequest === 'pending'">
40+
<button mat-raised-button color="primary" (click)="updateClick(element, 'accept')" i18n>
41+
<mat-icon>link</mat-icon>Accept
42+
</button>
43+
</ng-container>
44+
<button mat-raised-button color="warn" (click)="updateClick(element, 'delete')" i18n>
45+
<mat-icon>delete</mat-icon>Delete
46+
</button>
47+
</mat-cell>
48+
</ng-container>
49+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
50+
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
51+
</mat-table>
52+
<mat-paginator #paginator
53+
[pageSize]="50"
54+
[pageSizeOptions]="[5, 10, 20, 50, 100, 200]">
55+
</mat-paginator>
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Component, OnChanges, AfterViewInit, ViewChild, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
2+
import { CouchService } from '../shared/couchdb.service';
3+
import { DialogsPromptComponent } from '../shared/dialogs/dialogs-prompt.component';
4+
import { MatTableDataSource, MatPaginator, MatDialog, MatSort, MatDialogRef } from '@angular/material';
5+
import { switchMap, takeUntil } from 'rxjs/operators';
6+
import { forkJoin, of, Subject } from 'rxjs';
7+
import { filterSpecificFields, sortNumberOrString } from '../shared/table-helpers';
8+
import { DialogsViewComponent } from '../shared/dialogs/dialogs-view.component';
9+
import { DialogsListService } from '../shared/dialogs/dialogs-list.service';
10+
import { DialogsListComponent } from '../shared/dialogs/dialogs-list.component';
11+
import { StateService } from '../shared/state.service';
12+
13+
@Component({
14+
selector: 'planet-community-table',
15+
templateUrl: './community-table.component.html'
16+
})
17+
export class CommunityTableComponent implements OnChanges, AfterViewInit, OnDestroy {
18+
19+
@Input() data = [];
20+
@Input() hubs = [];
21+
@Input() hub: any = 'sandbox';
22+
@Output() requestUpdate = new EventEmitter<void>();
23+
communities = new MatTableDataSource();
24+
nations = [];
25+
displayedColumns = [
26+
'name',
27+
'code',
28+
'localDomain',
29+
'createdDate',
30+
'action'
31+
];
32+
editDialog: any;
33+
viewNationDetailDialog: any;
34+
dialogRef: MatDialogRef<DialogsListComponent>;
35+
onDestroy$ = new Subject<void>();
36+
planetType = this.stateService.configuration.planetType;
37+
38+
@ViewChild(MatPaginator) paginator: MatPaginator;
39+
@ViewChild(MatSort) sort: MatSort;
40+
41+
constructor(
42+
private couchService: CouchService,
43+
private dialogsListService: DialogsListService,
44+
private dialog: MatDialog,
45+
private stateService: StateService
46+
) {}
47+
48+
ngOnChanges() {
49+
this.communities.data = this.data;
50+
}
51+
52+
ngAfterViewInit() {
53+
this.communities.sortingDataAccessor = sortNumberOrString;
54+
this.communities.paginator = this.paginator;
55+
this.communities.sort = this.sort;
56+
}
57+
58+
ngOnDestroy() {
59+
this.onDestroy$.next();
60+
this.onDestroy$.complete();
61+
}
62+
63+
updateClick(community, change) {
64+
this.editDialog = this.dialog.open(DialogsPromptComponent, {
65+
data: {
66+
okClick: this.updateCommunity(community, change),
67+
changeType: change,
68+
type: 'community',
69+
displayName: community.name
70+
}
71+
});
72+
}
73+
74+
updateCommunity(community, change) {
75+
// Return a function with community on its scope to pass to delete dialog
76+
return () => {
77+
// With object destructuring colon means different variable name assigned, i.e. 'id' rather than '_id'
78+
// Split community object into id, rev, and all other props in communityInfo
79+
const { _id: communityId, _rev: communityRev, ...communityInfo } = community;
80+
switch (change) {
81+
case 'delete':
82+
this.deleteCommunity(community);
83+
break;
84+
case 'accept':
85+
forkJoin([
86+
// When accepting a registration request, add learner role to user from that community/nation,
87+
this.unlockUser(community),
88+
// update registration request to accepted
89+
this.couchService.put('communityregistrationrequests/' + communityId, { ...community, registrationRequest: 'accepted' })
90+
]).subscribe((data) => {
91+
this.requestUpdate.emit();
92+
this.editDialog.close();
93+
}, (error) => this.editDialog.componentInstance.message = 'Planet was not accepted');
94+
}
95+
};
96+
}
97+
98+
// Checks response and creates couch call if a doc was returned
99+
addDeleteObservable(res, db) {
100+
if (res.docs.length > 0) {
101+
const doc = res.docs[0];
102+
return this.couchService.delete(db + doc._id + '?rev=' + doc._rev);
103+
}
104+
return of({ 'ok': true });
105+
}
106+
107+
deleteCommunity(community) {
108+
// Return a function with community on its scope to pass to delete dialog
109+
const { _id: id, _rev: rev } = community;
110+
return this.pipeRemovePlanetUser(this.couchService.delete('communityregistrationrequests/' + id + '?rev=' + rev), community)
111+
.subscribe(([ data, userRes ]) => {
112+
// It's safer to remove the item from the array based on its id than to splice based on the index
113+
this.requestUpdate.emit();
114+
this.editDialog.close();
115+
}, (error) => this.editDialog.componentInstance.message = 'There was a problem deleting this community');
116+
}
117+
118+
pipeRemovePlanetUser(obs: any, community) {
119+
return obs.pipe(
120+
switchMap(data => {
121+
return forkJoin([ of(data), this.removePlanetUser(community) ]);
122+
})
123+
);
124+
}
125+
126+
removePlanetUser(community) {
127+
return forkJoin([
128+
this.couchService.post('_users/_find', { 'selector': { '_id': 'org.couchdb.user:' + community.adminName } }),
129+
this.couchService.post('shelf/_find', { 'selector': { '_id': 'org.couchdb.user:' + community.adminName } })
130+
]).pipe(switchMap(([ user, shelf ]) => {
131+
return forkJoin([
132+
this.addDeleteObservable(user, '_users/'),
133+
this.addDeleteObservable(shelf, 'shelf/')
134+
]);
135+
}));
136+
}
137+
138+
// Gives the requesting user the 'learner' role & access to all DBs (as of April 2018)
139+
unlockUser(community) {
140+
return this.couchService.post('_users/_find', { 'selector': { 'requestId': community._id } })
141+
.pipe(switchMap(data => {
142+
const user = data.docs[0];
143+
return this.couchService.put('_users/' + user._id + '?rev=' + user._rev,
144+
{ ...user, roles: [ 'learner' ] });
145+
}));
146+
}
147+
148+
view(planet) {
149+
this.viewNationDetailDialog = this.dialog.open(DialogsViewComponent, {
150+
width: '600px',
151+
autoFocus: false,
152+
data: {
153+
allData: planet,
154+
title: planet.planetType === 'nation' ? 'Nation Details' : 'Community Details'
155+
}
156+
});
157+
}
158+
159+
getChildPlanet(url: string) {
160+
this.dialogsListService.getListAndColumns('communityregistrationrequests',
161+
{ 'registrationRequest': 'accepted' }, url)
162+
.pipe(takeUntil(this.onDestroy$))
163+
.subscribe((planets) => {
164+
const data = {
165+
disableSelection: true,
166+
filterPredicate: filterSpecificFields([ 'name', 'code' ]),
167+
...planets };
168+
this.dialogRef = this.dialog.open(DialogsListComponent, {
169+
data: data,
170+
height: '500px',
171+
width: '600px',
172+
autoFocus: false
173+
});
174+
});
175+
}
176+
177+
addHubClick(planetCode, hubName) {
178+
const { children, ...hub } = this.hubs.find((hb: any) => hb.name === hubName);
179+
hub.spokes.push(planetCode);
180+
this.couchService.post('hubs', hub).pipe(switchMap(() => {
181+
if (this.hub !== 'sandbox') {
182+
return this.removeFromHub(planetCode);
183+
}
184+
return of({});
185+
})).subscribe(() => {
186+
this.requestUpdate.emit();
187+
});
188+
}
189+
190+
removeHubClick(planetCode) {
191+
this.removeFromHub(planetCode).subscribe(() => this.requestUpdate.emit());
192+
}
193+
194+
removeFromHub(planetCode) {
195+
return this.couchService.post('hubs', { ...this.hub, spokes: this.hub.spokes.filter(code => code !== planetCode) });
196+
}
197+
198+
}

src/app/community/community.component.html

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,53 @@
33
<button class="btnBack" mat-icon-button routerLink="/manager">
44
<mat-icon>arrow_back</mat-icon>
55
</button>
6-
<span i18n>Requests List</span>
6+
<span i18n>{ planetType, select, nation {Communities} center {Nations} }</span>
77
<span class="toolbar-fill"></span>
88
<mat-button-toggle-group
99
class="margin-lr-5 font-size-1"
10-
(change)="onFilterChange($event.value, 'registrationRequest')"
10+
(change)="shownStatusChange($event.value)"
1111
#filterGroup="matButtonToggleGroup">
12-
<mat-button-toggle value="pending" [checked]="this.filter.registrationRequest === 'pending'">
12+
<mat-button-toggle value="pending" [checked]="shownStatus === 'pending'">
1313
Pending
1414
</mat-button-toggle>
15-
<mat-button-toggle value="accepted" [checked]="this.filter.registrationRequest === 'accepted'">
15+
<mat-button-toggle value="accepted" [checked]="shownStatus === 'accepted'">
1616
Connected
1717
</mat-button-toggle>
1818
</mat-button-toggle-group>
1919
<mat-icon>search</mat-icon>
2020
<mat-form-field class="font-size-1">
21-
<input matInput (keyup)="requestListFilter($event.target.value)" [value]=searchValue i18n-placeholder placeholder="Search">
21+
<input matInput (keyup)="requestListFilter($event.target.value)" [value]="searchValue" i18n-placeholder placeholder="Search">
2222
</mat-form-field>
2323
</mat-toolbar>
2424

2525
<div class="space-container">
2626
<mat-toolbar>
2727
<mat-toolbar-row class="primary-color font-size-1">
28-
<span i18n>Your Current List of Requests</span>
28+
<span i18n>{ shownStatus, select,
29+
pending {Your Current List of Requests}
30+
accepted {{ planetType, select, nation {Connected Community Networks} center {Connected Regions} }} }
31+
</span>
32+
<span class="toolbar-fill"></span>
33+
<button *ngIf="shownStatus==='accepted'" mat-raised-button color="accent" i18n (click)="addHubClick()">
34+
{ planetType, select, nation {Add Network} center {Add Region} }
35+
</button>
2936
</mat-toolbar-row>
3037
</mat-toolbar>
31-
<div class="view-container view-full-height view-table">
32-
<mat-table #table [dataSource]="communities" matSort>
33-
<ng-container matColumnDef="name">
34-
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
35-
<mat-cell *matCellDef="let element">{{element.name}}</mat-cell>
36-
</ng-container>
37-
<ng-container matColumnDef="code">
38-
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>Code</mat-header-cell>
39-
<mat-cell *matCellDef="let element">{{element.code}}</mat-cell>
40-
</ng-container>
41-
<ng-container matColumnDef="localDomain">
42-
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>URL</mat-header-cell>
43-
<mat-cell *matCellDef="let element">{{element.localDomain}}</mat-cell>
44-
</ng-container>
45-
<ng-container matColumnDef="createdDate">
46-
<mat-header-cell *matHeaderCellDef mat-sort-header i18n>Created Date</mat-header-cell>
47-
<mat-cell *matCellDef="let element">{{element.createdDate | date: 'mediumDate'}}</mat-cell>
48-
</ng-container>
49-
<ng-container matColumnDef="action">
50-
<mat-header-cell *matHeaderCellDef i18n>Actions</mat-header-cell>
51-
<mat-cell *matCellDef="let element">
52-
<button mat-raised-button color="primary" (click)="view(element)" i18n>View</button>
53-
<ng-container *ngIf="element.registrationRequest === 'accepted'">
54-
<button *ngIf="element.planetType === 'nation'" mat-raised-button color="primary" (click)="getChildPlanet(element.localDomain)" i18n>
55-
Communities
56-
</button>
57-
</ng-container>
58-
<ng-container *ngIf="element.registrationRequest === 'pending'">
59-
<button mat-raised-button color="primary" (click)="updateClick(element, 'accept')" i18n>
60-
<mat-icon>link</mat-icon>Accept
61-
</button>
62-
</ng-container>
63-
<button mat-raised-button color="warn" (click)="updateClick(element, 'delete')" i18n>
64-
<mat-icon>delete</mat-icon>Delete
65-
</button>
66-
</mat-cell>
67-
</ng-container>
68-
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
69-
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
70-
</mat-table>
71-
<mat-paginator #paginator
72-
[pageSize]="50"
73-
[pageSizeOptions]="[5, 10, 20, 50, 100, 200]">
74-
</mat-paginator>
38+
<div class="view-container view-full-height" [ngClass]="{'view-table':shownStatus==='pending'}">
39+
<planet-community-table *ngIf="shownStatus==='pending'" [data]="filteredData" (requestUpdate)="getCommunityList()"></planet-community-table>
40+
<ng-container *ngIf="shownStatus==='accepted'">
41+
<mat-expansion-panel *ngFor="let hub of hubs">
42+
<mat-expansion-panel-header>
43+
<mat-panel-title>{{hub.name}}</mat-panel-title>
44+
</mat-expansion-panel-header>
45+
<planet-community-table [hubs]="hubs" [hub]="hub" [data]="hub.children" (requestUpdate)="getCommunityList()"></planet-community-table>
46+
</mat-expansion-panel>
47+
</ng-container>
48+
<mat-expansion-panel>
49+
<mat-expansion-panel-header>
50+
<mat-panel-title>Sandbox</mat-panel-title>
51+
</mat-expansion-panel-header>
52+
<planet-community-table [hubs]="hubs" [data]="sandboxPlanets" (requestUpdate)="getCommunityList()"></planet-community-table>
53+
</mat-expansion-panel>
7554
</div>
7655
</div>

0 commit comments

Comments
 (0)