Skip to content

Commit 5be8c03

Browse files
authored
Add organization members page (#10)
* seed page component * members component table base * access level instances fetched * members RUD functional * error service * track by
1 parent dcbd6cd commit 5be8c03

19 files changed

+590
-186
lines changed

src/@seed/api/cycle/cycle.service.ts

+23-18
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import type { HttpErrorResponse } from '@angular/common/http'
22
import { HttpClient } from '@angular/common/http'
33
import { inject, Injectable } from '@angular/core'
44
import type { Observable } from 'rxjs'
5-
import { BehaviorSubject, catchError, map, of, throwError } from 'rxjs'
5+
import { BehaviorSubject, catchError, map, tap } from 'rxjs'
66
import { OrganizationService } from '@seed/api/organization'
7+
import { ErrorService } from '@seed/services/error/error.service'
8+
import { SnackbarService } from 'app/core/snackbar/snackbar.service'
79
import type { Cycle, CycleResponse, CyclesResponse } from './cycle.types'
810

911
@Injectable({ providedIn: 'root' })
1012
export class CycleService {
1113
private _httpClient = inject(HttpClient)
1214
private _organizationService = inject(OrganizationService)
15+
private _snackBar = inject(SnackbarService)
16+
private _errorService = inject(ErrorService)
1317
private _cycles = new BehaviorSubject<Cycle[]>([])
1418
orgId: number
1519

@@ -25,45 +29,46 @@ export class CycleService {
2529
.get<CyclesResponse>(url)
2630
.pipe(
2731
map((response) => response.cycles),
28-
catchError((error) => {
29-
console.error('Error fetching cycles:', error)
30-
return of([])
32+
tap((cycles) => {
33+
this._cycles.next(cycles)
34+
}),
35+
catchError((error: HttpErrorResponse) => {
36+
return this._errorService.handleError(error, 'Error fetching cycles')
3137
}),
3238
)
33-
.subscribe((cycles) => {
34-
this._cycles.next(cycles)
35-
})
39+
.subscribe()
3640
})
3741
}
3842

3943
post({ data, orgId }): Observable<CycleResponse | null> {
40-
// create a cycle
4144
const url = `/api/v3/cycles/?organization_id=${orgId}`
4245
return this._httpClient.post<CycleResponse>(url, data).pipe(
43-
map((response) => response),
44-
catchError(({ error }: { error: HttpErrorResponse }) => {
45-
console.error('Error creating cycle:', error)
46-
return throwError(() => new Error(error?.message || 'Error creating cycle'))
46+
tap((response) => {
47+
this._snackBar.success(`Created Cycle ${response.cycles.name}`)
48+
}),
49+
catchError((error: HttpErrorResponse) => {
50+
return this._errorService.handleError(error, 'Error creating cycle')
4751
}),
4852
)
4953
}
5054

5155
put({ data, id, orgId }): Observable<CycleResponse | null> {
5256
const url = `/api/v3/cycles/${id}/?organization_id=${orgId}`
5357
return this._httpClient.put<CycleResponse>(url, data).pipe(
54-
catchError(({ error }: { error: HttpErrorResponse }) => {
55-
console.error('Error updating cycle:', error)
56-
return throwError(() => new Error(error?.message || 'Error updating cycle'))
58+
tap((response) => {
59+
this._snackBar.success(`Updated Cycle ${response.cycles.name}`)
60+
}),
61+
catchError((error: HttpErrorResponse) => {
62+
return this._errorService.handleError(error, 'Error updating cycle')
5763
}),
5864
)
5965
}
6066

6167
delete(id: number, orgId: number) {
6268
const url = `/api/v3/cycles/${id}/?organization_id=${orgId}`
6369
return this._httpClient.delete(url).pipe(
64-
catchError(({ error }: { error: HttpErrorResponse }) => {
65-
console.error('Error deleting cycle:', error)
66-
return throwError(() => new Error(error?.message || 'Error deleting cycle'))
70+
catchError((error: HttpErrorResponse) => {
71+
return this._errorService.handleError(error, 'Error deleting cycle')
6772
}),
6873
)
6974
}

src/@seed/api/organization/organization.service.ts

+74-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ import { HttpClient } from '@angular/common/http'
33
import { inject, Injectable } from '@angular/core'
44
import type { Observable } from 'rxjs'
55
import { catchError, map, of, ReplaySubject, Subject, takeUntil, tap } from 'rxjs'
6+
import { ErrorService } from '@seed/services/error/error.service'
67
import { SnackbarService } from 'app/core/snackbar/snackbar.service'
78
import { naturalSort } from '../../utils'
89
import { UserService } from '../user'
910
import type {
11+
AccessLevelNode,
12+
AccessLevelsByDepth,
13+
AccessLevelTree,
14+
AccessLevelTreeResponse,
1015
BriefOrganization,
1116
Organization,
1217
OrganizationResponse,
1318
OrganizationSettings,
1419
OrganizationsResponse,
20+
OrganizationUser,
21+
OrganizationUsersResponse,
1522
} from './organization.types'
1623

1724
@Injectable({ providedIn: 'root' })
@@ -20,11 +27,17 @@ export class OrganizationService {
2027
private _userService = inject(UserService)
2128
private _organizations = new ReplaySubject<BriefOrganization[]>(1)
2229
private _currentOrganization = new ReplaySubject<Organization>(1)
30+
private _organizationUsers = new ReplaySubject<OrganizationUser[]>(1)
31+
private _accessLevelTree = new ReplaySubject<AccessLevelTree>(1)
32+
private _accessLevelInstancesByDepth: AccessLevelsByDepth = {}
33+
private _errorService = inject(ErrorService)
2334
private readonly _unsubscribeAll$ = new Subject<void>()
2435
private _snackBar = inject(SnackbarService)
2536

2637
organizations$ = this._organizations.asObservable()
2738
currentOrganization$ = this._currentOrganization.asObservable()
39+
organizationUsers$ = this._organizationUsers.asObservable()
40+
accessLevelTree$ = this._accessLevelTree.asObservable()
2841

2942
constructor() {
3043
// Fetch current org data whenever user org id changes
@@ -54,8 +67,53 @@ export class OrganizationService {
5467
}),
5568
catchError((error: HttpErrorResponse) => {
5669
// TODO need to figure out error handling
57-
console.error('Error occurred fetching organization: ', error.error)
58-
return of({} as Organization)
70+
return this._errorService.handleError(error, 'Error fetching organization')
71+
}),
72+
)
73+
}
74+
75+
getOrganizationUsers(orgId: number): void {
76+
const url = `/api/v3/organizations/${orgId}/users/`
77+
this._httpClient.get<OrganizationUsersResponse>(url)
78+
.pipe(
79+
map((response) => response.users.sort((a, b) => naturalSort(a.last_name, b.last_name))),
80+
tap((users) => { this._organizationUsers.next(users) }),
81+
catchError((error: HttpErrorResponse) => {
82+
return this._errorService.handleError(error, 'Error fetching organization users')
83+
}),
84+
).subscribe()
85+
}
86+
87+
getOrganizationAccessLevelTree(orgId: number): void {
88+
const url = `/api/v3/organizations/${orgId}/access_levels/tree`
89+
this._httpClient.get<AccessLevelTreeResponse>(url)
90+
.pipe(
91+
map((response) => {
92+
// update response to include more usable accessLevelInstancesByDepth
93+
this._accessLevelInstancesByDepth = this._calculateAccessLevelInstancesByDepth(response.access_level_tree, 0)
94+
return {
95+
accessLevelNames: response.access_level_names,
96+
accessLevelInstancesByDepth: this._accessLevelInstancesByDepth,
97+
}
98+
}),
99+
tap((accessLevelTree) => {
100+
this._accessLevelTree.next(accessLevelTree)
101+
}),
102+
catchError((error: HttpErrorResponse) => {
103+
return this._errorService.handleError(error, 'Error fetching organization access level tree')
104+
}),
105+
)
106+
.subscribe()
107+
}
108+
109+
deleteOrganizationUser(userId: number, orgId: number) {
110+
const url = `/api/v3/organizations/${orgId}/users/${userId}/remove/`
111+
return this._httpClient.delete(url).pipe(
112+
tap(() => {
113+
this._snackBar.success('Member removed from organization')
114+
}),
115+
catchError((error: HttpErrorResponse) => {
116+
return this._errorService.handleError(error, 'Error removing member from organization')
59117
}),
60118
)
61119
}
@@ -81,13 +139,24 @@ export class OrganizationService {
81139
})
82140
}),
83141
catchError((error: HttpErrorResponse) => {
84-
console.error('Error occurred fetching organization: ', error.error)
85-
this._snackBar.alert(`An error occurred updating the organization: ${error.error}`)
86-
return of(null)
142+
return this._errorService.handleError(error, 'Error updating organization settings')
87143
}),
88144
)
89145
}
90146

147+
/*
148+
* Transform access level tree into a more usable format
149+
*/
150+
private _calculateAccessLevelInstancesByDepth(tree: AccessLevelNode[], depth: number, result: AccessLevelsByDepth = {}): AccessLevelsByDepth {
151+
if (!tree) return result
152+
if (!result[depth]) result[depth] = []
153+
for (const ali of tree) {
154+
result[depth].push({ id: ali.id, name: ali.name })
155+
this._calculateAccessLevelInstancesByDepth(ali.children, depth + 1, result)
156+
}
157+
return result
158+
}
159+
91160
private _get(brief = false): Observable<(BriefOrganization | Organization)[]> {
92161
const url = brief ? '/api/v3/organizations/?brief=true' : '/api/v3/organizations/'
93162
return this._httpClient.get<OrganizationsResponse>(url).pipe(

src/@seed/api/organization/organization.types.ts

+37
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,40 @@ export type OrganizationResponse = {
8686
export type OrganizationsResponse = {
8787
organizations: (BriefOrganization | Organization)[];
8888
}
89+
90+
export type OrganizationUser = {
91+
access_level: string;
92+
access_level_instance_id: number;
93+
access_level_instance_name: string;
94+
email: string;
95+
first_name: string;
96+
last_name: string;
97+
number_of_orgs: number;
98+
role: UserRole;
99+
user_id: number;
100+
}
101+
102+
export type OrganizationUsersResponse = {
103+
users: OrganizationUser[];
104+
status: string;
105+
}
106+
107+
export type AccessLevelTreeResponse = {
108+
access_level_names: string[];
109+
access_level_tree: AccessLevelNode[];
110+
}
111+
112+
export type AccessLevelTree = {
113+
accessLevelNames: string[];
114+
accessLevelInstancesByDepth: AccessLevelsByDepth;
115+
}
116+
117+
export type AccessLevelNode = {
118+
id: number;
119+
name: string;
120+
organization: number;
121+
path: Record<string, string>;
122+
children: AccessLevelNode[];
123+
}
124+
125+
export type AccessLevelsByDepth = Record<string, { id: number; name: string }[]>

src/@seed/api/user/user.service.ts

+37-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { HttpErrorResponse } from '@angular/common/http'
22
import { HttpClient } from '@angular/common/http'
33
import { inject, Injectable } from '@angular/core'
44
import type { Observable } from 'rxjs'
5-
import { catchError, distinctUntilChanged, ReplaySubject, switchMap, take, tap, throwError } from 'rxjs'
5+
import { catchError, distinctUntilChanged, ReplaySubject, switchMap, take, tap } from 'rxjs'
66
import type {
77
CurrentUser,
88
GenerateApiKeyResponse,
@@ -11,12 +11,14 @@ import type {
1111
SetDefaultOrganizationResponse,
1212
UserUpdateRequest,
1313
} from '@seed/api/user'
14+
import { ErrorService } from '@seed/services/error/error.service'
1415

1516
@Injectable({ providedIn: 'root' })
1617
export class UserService {
1718
private _httpClient = inject(HttpClient)
1819
private _currentOrganizationId = new ReplaySubject<number>(1)
1920
private _currentUser = new ReplaySubject<CurrentUser>(1)
21+
private _errorService = inject(ErrorService)
2022
currentOrganizationId$ = this._currentOrganizationId.asObservable().pipe(distinctUntilChanged())
2123
currentUser$ = this._currentUser.asObservable()
2224

@@ -55,15 +57,39 @@ export class UserService {
5557
* Update user
5658
*/
5759
updateUser(userId: number, params: UserUpdateRequest): Observable<CurrentUser> {
58-
return this._httpClient.put<CurrentUser>(`api/v3/users/${userId}/`, params).pipe(
59-
tap((user) => {
60-
this._currentUser.next(user)
61-
}),
62-
catchError((error: HttpErrorResponse) => {
63-
console.error('Error occurred while updating user:', error.error)
64-
return this._currentUser
65-
}),
66-
)
60+
return this._httpClient.put<CurrentUser>(`api/v3/users/${userId}/`, params)
61+
.pipe(
62+
tap((user) => {
63+
this._currentUser.next(user)
64+
}),
65+
catchError((error: HttpErrorResponse) => {
66+
return this._errorService.handleError(error, 'Error updating user')
67+
}),
68+
)
69+
}
70+
71+
/**
72+
* Update user role
73+
*/
74+
updateUserRole(userId: number, orgId: number, role: string): Observable<{ status: string }> {
75+
const url = `api/v3/users/${userId}/role/?organization_id=${orgId}`
76+
return this._httpClient.put<{ status: string }>(url, { role })
77+
.pipe(
78+
catchError((error: HttpErrorResponse) => {
79+
return this._errorService.handleError(error, 'Error updating user role')
80+
}),
81+
)
82+
}
83+
84+
updateUserAccessLevelInstance(userId: number, orgId: number, accessLevelInstanceId: number): Observable<{ status: string }> {
85+
console.log('update access level instance', userId, orgId, accessLevelInstanceId)
86+
const url = `/api/v3/users/${userId}/access_level_instance/?organization_id=${orgId}`
87+
return this._httpClient.put<{ status: string }>(url, { access_level_instance_id: accessLevelInstanceId })
88+
.pipe(
89+
catchError((error: HttpErrorResponse) => {
90+
return this._errorService.handleError(error, 'Error updating user access level instance')
91+
}),
92+
)
6793
}
6894

6995
/**
@@ -79,7 +105,7 @@ export class UserService {
79105
this.getCurrentUser().subscribe()
80106
}),
81107
catchError((error: HttpErrorResponse) => {
82-
return throwError(() => error)
108+
return this._errorService.handleError(error, 'Error updating password')
83109
}),
84110
)
85111
}

src/@seed/components/page/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './page.component'
2+
export * from './table/table-container.component'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="flex-auto pt-4 sm:pt-6">
2+
<div class="mx-auto w-full max-w-screen-xl">
3+
<div class="grid w-full min-w-0 grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-4">
4+
<div class="bg-card flex flex-auto flex-col overflow-hidden rounded-2xl p-6 shadow sm:col-span-2 md:col-span-4">
5+
<div class="mx-6 overflow-x-auto">
6+
<ng-content></ng-content>
7+
</div>
8+
</div>
9+
</div>
10+
</div>
11+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Component } from '@angular/core'
2+
3+
@Component({
4+
selector: 'seed-page-table-container',
5+
templateUrl: './table-container.component.html',
6+
imports: [],
7+
})
8+
export class TableContainerComponent {
9+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { HttpErrorResponse } from '@angular/common/http'
2+
import { inject, Injectable } from '@angular/core'
3+
import { throwError } from 'rxjs'
4+
import { SnackbarService } from 'app/core/snackbar/snackbar.service'
5+
6+
@Injectable({ providedIn: 'root' })
7+
export class ErrorService {
8+
private _snackBar = inject(SnackbarService)
9+
10+
handleError(error: HttpErrorResponse, defaultMessage: string) {
11+
const errorMessage = (error.error as { message: string })?.message || defaultMessage
12+
this._snackBar.alert(errorMessage)
13+
return throwError(() => new Error(error?.message || defaultMessage))
14+
}
15+
}

0 commit comments

Comments
 (0)