Skip to content

Commit c6ba28f

Browse files
perryr16Ross Perrycrutankflemin
authored
Add Organizations pages ui and routing (#1)
* initial organizations pages ui organizations routing base org name placeholder styles * fetch current org * non inventory org pages non inventory org pages * inventory specific pages * moved navigation to sidebar * remove org id from org rotues * prevent nav collapse on /taxlots * organization backend now active * now with async pipe for simpler componentry * organizations routing base organizations routing base org name placeholder styles * fetch current org non inventory org pages * inventory specific pages * moved navigation to sidebar * remove org id from org rotues * prevent nav collapse on /taxlots * organization backend now active * now with async pipe for simpler componentry * org cycles page * cycles header --------- Co-authored-by: Ross Perry <ross.perry@Rosss-MacBook-Pro-2.local> Co-authored-by: Caleb Rutan <caleb.rutan@deptagency.com> Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
1 parent df6e89b commit c6ba28f

File tree

39 files changed

+943
-13
lines changed

39 files changed

+943
-13
lines changed

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

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { HttpClient } from '@angular/common/http'
2+
import { inject, Injectable } from '@angular/core'
3+
import { BehaviorSubject, map } from 'rxjs'
4+
import { OrganizationService } from '@seed/api/organization'
5+
import type { Cycle, ListCyclesResponse } from './cycle.types'
6+
7+
@Injectable({ providedIn: 'root' })
8+
export class CycleService {
9+
private _httpClient = inject(HttpClient)
10+
private _organizationService = inject(OrganizationService)
11+
private _cycles = new BehaviorSubject<Cycle[]>([])
12+
13+
cycles$ = this._cycles.asObservable()
14+
15+
get(): void {
16+
// fetch current organization
17+
this._organizationService.currentOrganization$.subscribe(({ org_id }) => {
18+
const url = `/api/v3/cycles/?organization_id=${org_id}`
19+
// fetch cycles
20+
this._httpClient
21+
.get<ListCyclesResponse>(url)
22+
.pipe(map((response) => response.cycles))
23+
.subscribe({
24+
next: (cycles) => {
25+
this._cycles.next(cycles)
26+
},
27+
error: (error) => {
28+
console.error('Error fetching cycles:', error)
29+
},
30+
})
31+
})
32+
}
33+
}

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

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,57 @@
1+
import type { HttpErrorResponse } from '@angular/common/http'
12
import { HttpClient } from '@angular/common/http'
23
import { inject, Injectable } from '@angular/core'
34
import type { Observable } from 'rxjs'
4-
import { map, ReplaySubject, tap } from 'rxjs'
5+
import { catchError, map, of, ReplaySubject, Subject, takeUntil, tap } from 'rxjs'
56
import { naturalSort } from '../../utils'
6-
import type { BriefOrganization, Organization, OrganizationsResponse } from './organization.types'
7+
import { UserService } from '../user'
8+
import type { BriefOrganization, Organization, OrganizationResponse, OrganizationsResponse } from './organization.types'
79

810
@Injectable({ providedIn: 'root' })
911
export class OrganizationService {
1012
private _httpClient = inject(HttpClient)
13+
private _userService = inject(UserService)
1114
private _organizations = new ReplaySubject<BriefOrganization[]>(1)
15+
private _currentOrganization = new ReplaySubject<Organization>(1)
16+
private readonly _unsubscribeAll$ = new Subject<void>()
17+
1218
organizations$ = this._organizations.asObservable()
19+
currentOrganization$ = this._currentOrganization.asObservable()
20+
21+
constructor() {
22+
// Fetch current org data whenever user org id changes
23+
this._userService.currentOrganizationId$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((organizationId) => {
24+
this.getById(organizationId).subscribe()
25+
})
26+
}
1327

14-
get(): Observable<Organization[]> {
15-
return this._get(false) as Observable<Organization[]>
28+
get(org_id?: number): Observable<Organization[]> | Observable<Organization> {
29+
if (org_id) {
30+
return this.getById(org_id)
31+
} else {
32+
return this._get(false) as Observable<Organization[]>
33+
}
1634
}
1735

1836
getBrief(): Observable<BriefOrganization[]> {
1937
return this._get(true)
2038
}
2139

40+
getById(org_id: number): Observable<Organization> {
41+
const url = `/api/v3/organizations/${org_id}/`
42+
return this._httpClient.get<OrganizationResponse>(url).pipe(
43+
map((or) => {
44+
this._currentOrganization.next(or.organization)
45+
return or.organization
46+
}),
47+
catchError((error: HttpErrorResponse) => {
48+
// TODO need to figure out error handling
49+
console.error('Error occurred fetching organization: ', error.error)
50+
return of({} as Organization)
51+
}),
52+
)
53+
}
54+
2255
private _get(brief = false): Observable<(BriefOrganization | Organization)[]> {
2356
const url = brief ? '/api/v3/organizations/?brief=true' : '/api/v3/organizations/'
2457
return this._httpClient.get<OrganizationsResponse>(url).pipe(

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

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export type Organization = BriefOrganization & {
7373
default_reports_y_axis_options: Column[];
7474
require_2fa: boolean;
7575
}
76+
export type OrganizationResponse = {
77+
status: string;
78+
organization: Organization;
79+
}
7680
export type OrganizationsResponse = {
7781
organizations: (BriefOrganization | Organization)[];
7882
}

src/@seed/components/navigation/horizontal/components/branch/branch.component.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { NgClass, NgTemplateOutlet } from '@angular/common'
22
import type { OnDestroy, OnInit } from '@angular/core'
3-
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input, viewChild } from '@angular/core'
3+
import {
4+
booleanAttribute,
5+
ChangeDetectionStrategy,
6+
ChangeDetectorRef,
7+
Component,
8+
forwardRef,
9+
inject,
10+
input,
11+
viewChild,
12+
} from '@angular/core'
413
import { MatIconModule } from '@angular/material/icon'
514
import type { MatMenu } from '@angular/material/menu'
615
import { MatMenuModule } from '@angular/material/menu'
@@ -18,6 +27,7 @@ import type { NavigationItem } from '@seed/components/navigation/navigation.type
1827
changeDetection: ChangeDetectionStrategy.OnPush,
1928
imports: [
2029
HorizontalNavigationBasicItemComponent,
30+
forwardRef(() => HorizontalNavigationBranchItemComponent),
2131
HorizontalNavigationDividerItemComponent,
2232
MatIconModule,
2333
MatMenuModule,

src/@seed/components/navigation/vertical/components/collapsible/collapsible.component.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import type { BooleanInput } from '@angular/cdk/coercion'
12
import { NgClass } from '@angular/common'
23
import type { OnDestroy, OnInit } from '@angular/core'
3-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, inject, input } from '@angular/core'
4+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostBinding, inject, input } from '@angular/core'
45
import { MatIconModule } from '@angular/material/icon'
56
import { MatTooltipModule } from '@angular/material/tooltip'
67
import { NavigationEnd, Router } from '@angular/router'
@@ -26,12 +27,15 @@ import { exactMatchOptions, subsetMatchOptions } from '@seed/utils'
2627
MatTooltipModule,
2728
MatIconModule,
2829
VerticalNavigationBasicItemComponent,
30+
forwardRef(() => VerticalNavigationCollapsibleItemComponent),
2931
VerticalNavigationDividerItemComponent,
3032
VerticalNavigationGroupItemComponent,
3133
VerticalNavigationSpacerItemComponent,
3234
],
3335
})
3436
export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDestroy {
37+
static ngAcceptInputType_autoCollapse: BooleanInput
38+
3539
private _changeDetectorRef = inject(ChangeDetectorRef)
3640
private _router = inject(Router)
3741
private _navigationService = inject(SeedNavigationService)
@@ -45,6 +49,9 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
4549
private _verticalNavigationComponent: VerticalNavigationComponent
4650
private readonly _unsubscribeAll$ = new Subject<void>()
4751

52+
/**
53+
* Host binding for component classes
54+
*/
4855
@HostBinding('class') get classList(): Record<string, boolean> {
4956
return {
5057
'seed-vertical-navigation-item-collapsed': this.isCollapsed,
@@ -61,7 +68,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
6168
this.expand()
6269
} else {
6370
// If the autoCollapse is on, collapse...
64-
if (this.autoCollapse()) {
71+
if (this.autoCollapse) {
6572
this.collapse()
6673
}
6774
}
@@ -80,7 +87,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
8087
})
8188

8289
// Listen for the onCollapsibleItemExpanded from the service if the autoCollapse is on
83-
if (this.autoCollapse()) {
90+
if (this.autoCollapse) {
8491
this._verticalNavigationComponent.onCollapsibleItemExpanded.pipe(takeUntil(this._unsubscribeAll$)).subscribe((expandedItem) => {
8592
// Check if the expanded item is null
8693
if (expandedItem === null) {
@@ -119,7 +126,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
119126
this.expand()
120127
} else {
121128
// If the autoCollapse is on, collapse...
122-
if (this.autoCollapse()) {
129+
if (this.autoCollapse) {
123130
this.collapse()
124131
}
125132
}
@@ -222,7 +229,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
222229
}
223230

224231
// Check if the child has a link and is active
225-
if (child.link && this._router.isActive(child.link, child.exactMatch ? exactMatchOptions : subsetMatchOptions)) {
232+
if (child.link && this._router.isActive(child.id, child.exactMatch ? exactMatchOptions : subsetMatchOptions)) {
226233
return true
227234
}
228235
}

src/@seed/components/navigation/vertical/components/group/group.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NgClass } from '@angular/common'
22
import type { OnDestroy, OnInit } from '@angular/core'
3-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input } from '@angular/core'
3+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, inject, input } from '@angular/core'
44
import { MatIconModule } from '@angular/material/icon'
55
import { Subject, takeUntil } from 'rxjs'
66
import type { NavigationItem, VerticalNavigationComponent } from '@seed/components'
@@ -22,6 +22,7 @@ import {
2222
VerticalNavigationBasicItemComponent,
2323
VerticalNavigationCollapsibleItemComponent,
2424
VerticalNavigationDividerItemComponent,
25+
forwardRef(() => VerticalNavigationGroupItemComponent),
2526
VerticalNavigationSpacerItemComponent,
2627
],
2728
})

src/app/app.routes.ts

+6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export const appRoutes: Route[] = [
7575
},
7676
{ path: 'contact', title: 'Contact', component: ContactComponent },
7777
{ path: 'about', title: 'About', component: AboutComponent },
78+
{ path: 'contact', title: 'Contact', component: ContactComponent },
79+
{ path: 'documentation', title: 'Documentation', component: DocumentationComponent },
80+
{
81+
path: 'organizations',
82+
loadChildren: () => import('app/modules/organizations/organizations.routes'),
83+
},
7884
// 404, redirect to dashboard
7985
{ path: '**', redirectTo: 'dashboard' },
8086
],

src/app/core/navigation/navigation.service.ts

+77-1
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,85 @@ export class NavigationService {
2929
{
3030
id: 'organizations',
3131
title: 'Organizations',
32-
type: 'basic',
32+
type: 'collapsible',
3333
icon: 'fa-solid:users',
3434
link: '/organizations',
35+
children: [
36+
{
37+
id: 'organizations/settings',
38+
link: 'organizations/settings',
39+
title: 'Settings',
40+
icon: 'fa-solid:gears',
41+
type: 'basic',
42+
},
43+
{
44+
id: 'organizations/access-level-tree',
45+
link: '/organizations/access-level-tree',
46+
title: 'Access Level Tree',
47+
icon: 'fa-solid:sitemap',
48+
type: 'basic',
49+
},
50+
{
51+
id: 'organizations/column-mappings',
52+
link: '/organizations/column-mappings/properties',
53+
title: 'Column mappings',
54+
icon: 'fa-solid:right-left',
55+
type: 'basic',
56+
regexMatch: /^\/organizations\/column-mappings\/(properties|taxlots)/,
57+
},
58+
{
59+
id: 'organizations/column-settings',
60+
link: '/organizations/column-settings/properties',
61+
title: 'Column Settings',
62+
icon: 'fa-solid:sliders',
63+
type: 'basic',
64+
regexMatch: /^\/organizations\/column-settings\/(properties|taxlots)/,
65+
},
66+
{
67+
id: 'organizations/cycles',
68+
link: '/organizations/cycles',
69+
title: 'Cycles',
70+
icon: 'fa-solid:calendar-days',
71+
type: 'basic',
72+
},
73+
{
74+
id: 'organizations/data-quality',
75+
link: '/organizations/data-quality/properties',
76+
title: 'Data Quality',
77+
icon: 'fa-solid:flag',
78+
type: 'basic',
79+
regexMatch: /^\/organizations\/data-quality\/(properties|taxlots|goal)/,
80+
},
81+
{
82+
id: 'organizations/derived-columns',
83+
link: '/organizations/derived-columns/properties',
84+
title: 'Derived Columns',
85+
icon: 'fa-solid:calculator',
86+
type: 'basic',
87+
regexMatch: /^\/organizations\/derived-columns\/(properties|taxlots)/,
88+
},
89+
{
90+
id: 'organizations/email-templates',
91+
link: '/organizations/email-templates',
92+
title: 'Email Templates',
93+
icon: 'fa-solid:envelope',
94+
type: 'basic',
95+
},
96+
{
97+
id: 'organizations/labels',
98+
link: '/organizations/labels',
99+
title: 'Labels',
100+
icon: 'fa-solid:tags',
101+
type: 'basic',
102+
},
103+
{
104+
id: 'organizations/members',
105+
link: '/organizations/members',
106+
title: 'Members',
107+
icon: 'fa-solid:user',
108+
type: 'basic',
109+
},
110+
],
35111
},
36112
{
37113
id: 'insights',

src/app/layout/layouts/main/main.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class MainLayoutComponent implements OnInit, OnDestroy {
5151
this.isScreenSmall = !matchingAliases.includes('md')
5252

5353
// Change the navigation appearance
54-
this.navigationAppearance = this.isScreenSmall ? 'default' : 'dense'
54+
this.navigationAppearance = this.isScreenSmall ? 'dense' : 'default'
5555
})
5656

5757
this._versionService.version$.pipe(takeUntil(this._unsubscribeAll$)).subscribe(({ version, sha }) => {
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export * from './organizations-access-level-tree/organizations-access-level-tree.component'
2+
export * from './organizations-column-mappings/organizations-column-mappings.component'
3+
export * from './organizations-column-settings/organizations-column-settings.component'
4+
export * from './organizations-cycles/organizations-cycles.component'
5+
export * from './organizations-data-quality/organizations-data-quality.component'
6+
export * from './organizations-derived-columns/organizations-derived-columns.component'
7+
export * from './organizations-email-templates/organizations-email-templates.component'
8+
export * from './organizations-labels/organizations-labels.component'
9+
export * from './organizations-members/organizations-members.component'
10+
export * from './organizations-list/organizations-list.component'
11+
export * from './organizations-nav/organizations-nav.component'
12+
export * from './organizations-settings/organizations-settings.component'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="prose">Access Level Tree Content</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { OnInit } from '@angular/core'
2+
import { Component } from '@angular/core'
3+
import { MatIconModule } from '@angular/material/icon'
4+
5+
@Component({
6+
selector: 'seed-organizations-access-level-tree',
7+
templateUrl: './organizations-access-level-tree.component.html',
8+
imports: [MatIconModule],
9+
})
10+
export class OrganizationsAccessLevelTreeComponent implements OnInit {
11+
ngOnInit(): void {
12+
console.log('organizations access level tree')
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<div class="flex min-w-0 flex-auto flex-col" *transloco="let t">
2+
<div class="flex space-x-1">
3+
@for (tab of tabs; track tab) {
4+
<div
5+
class="bg-default relative flex cursor-pointer self-start overflow-hidden rounded-t-xl border border-b-0 pb-1 pl-5 pr-4 pt-2"
6+
[ngClass]="type === tab ? 'z-2' : ['bg-slate-50', 'dark:bg-slate-700']"
7+
(click)="toggleInventoryType(tab)"
8+
matRipple
9+
>
10+
<div class="overflow-hidden">
11+
<div class="truncate font-medium leading-6">{{ t(tab) }}</div>
12+
</div>
13+
</div>
14+
}
15+
</div>
16+
17+
<div class="z-1 -mt-px flex-auto border-t pt-4 sm:pt-6">
18+
<div class="mx-auto w-full max-w-screen-xl">{{ t(type) }} column mappings table</div>
19+
</div>
20+
</div>

0 commit comments

Comments
 (0)